Refactor MCP services and adopt slash-based screen paths
Extract core MCP logic into modular services (ResolveScreenPath, RenderScreenNarrative, ExecuteScreenAction) and update screen path conventions to use slash notation (e.g., /PopCommerce/Product) instead of dot notation. This aligns MCP navigation with browser URLs and improves path resolution reliability. - Split McpServices.xml into specialized services for better maintainability - Update DefaultScreenMacros.mcp.ftl to generate slash-based links - Update prompts and documentation to reflect new path convention - Enhance CustomScreenTestImpl to support slash path parsing - Add AGENTS.md documenting self-guided narrative screens architecture
Showing
9 changed files
with
420 additions
and
53 deletions
AGENTS.md
0 → 100644
This diff is collapsed.
Click to expand it.
| ... | @@ -67,21 +67,21 @@ For ${focus?.capitalize() ?: 'General'} operations: | ... | @@ -67,21 +67,21 @@ For ${focus?.capitalize() ?: 'General'} operations: |
| 67 | 67 | ||
| 68 | ${focus == 'catalog' ? ''' | 68 | ${focus == 'catalog' ? ''' |
| 69 | ## Catalog Operations | 69 | ## Catalog Operations |
| 70 | - **FindProduct**: Use `PopCommerce.PopCommerceAdmin.Catalog.Product.FindProduct` for product catalog | 70 | - **FindProduct**: Use `/PopCommerce/PopCommerceAdmin.Catalog.Product.FindProduct` for product catalog |
| 71 | - **FindFeature**: Use `PopCommerce.PopCommerceAdmin.Catalog.Feature.FindFeature` to search by features like color or size | 71 | - **FindFeature**: Use `/PopCommerce/PopCommerceAdmin.Catalog.Feature.FindFeature` to search by features like color or size |
| 72 | - **EditPrices**: Use `PopCommerce.PopCommerceAdmin.Catalog.Product.EditPrices` to check prices | 72 | - **EditPrices**: Use `/PopCommerce/PopCommerceAdmin.Catalog.Product.EditPrices` to check prices |
| 73 | ''' : ''} | 73 | ''' : ''} |
| 74 | 74 | ||
| 75 | ${focus == 'orders' ? ''' | 75 | ${focus == 'orders' ? ''' |
| 76 | ## Order Operations | 76 | ## Order Operations |
| 77 | - **FindOrder**: Use `PopCommerce.PopCommerceAdmin.Order.FindOrder` for order status and lookup | 77 | - **FindOrder**: Use `/PopCommerce/PopCommerceAdmin.Order.FindOrder` for order status and lookup |
| 78 | - **QuickSearch**: Use `PopCommerce.PopCommerceAdmin.QuickSearch` for general order searches | 78 | - **QuickSearch**: Use `/PopCommerce/PopCommerceAdmin.QuickSearch` for general order searches |
| 79 | ''' : ''} | 79 | ''' : ''} |
| 80 | 80 | ||
| 81 | ${focus == 'customers' ? ''' | 81 | ${focus == 'customers' ? ''' |
| 82 | ## Customer Operations | 82 | ## Customer Operations |
| 83 | - **CustomerRoot**: Use `PopCommerce.PopCommerceRoot.Customer` for customer management | 83 | - **CustomerRoot**: Use `/PopCommerce/PopCommerceRoot.Customer` for customer management |
| 84 | - **QuickSearch**: Use `PopCommerce.PopCommerceAdmin.QuickSearch` for customer lookup | 84 | - **QuickSearch**: Use `/PopCommerce/PopCommerceAdmin.QuickSearch` for customer lookup |
| 85 | ''' : ''} | 85 | ''' : ''} |
| 86 | 86 | ||
| 87 | ${detailLevel == 'advanced' ? ''' | 87 | ${detailLevel == 'advanced' ? ''' | ... | ... |
| ... | @@ -45,23 +45,23 @@ Use the following discovery tools to explore available functionality: | ... | @@ -45,23 +45,23 @@ Use the following discovery tools to explore available functionality: |
| 45 | ## Common Screen Paths | 45 | ## Common Screen Paths |
| 46 | 46 | ||
| 47 | ### Catalog Operations | 47 | ### Catalog Operations |
| 48 | - `PopCommerce.PopCommerceAdmin.Catalog.Product.FindProduct`: Search and browse products | 48 | - `/PopCommerce/PopCommerceAdmin.Catalog.Product.FindProduct`: Search and browse products |
| 49 | - `PopCommerce.PopCommerceAdmin.Catalog.Feature.FindFeature`: Search by features like color or size | 49 | - `/PopCommerce/PopCommerceAdmin.Catalog.Feature.FindFeature`: Search by features like color or size |
| 50 | - `PopCommerce.PopCommerceAdmin.Catalog.Product.EditPrices`: View and update product prices | 50 | - `/PopCommerce/PopCommerceAdmin.Catalog.Product.EditPrices`: View and update product prices |
| 51 | 51 | ||
| 52 | ### Order Management | 52 | ### Order Management |
| 53 | - `PopCommerce.PopCommerceAdmin.Order.FindOrder`: Lookup order status and details | 53 | - `/PopCommerce/PopCommerceAdmin.Order.FindOrder`: Lookup order status and details |
| 54 | - `PopCommerce.PopCommerceAdmin.QuickSearch`: General order and customer search | 54 | - `/PopCommerce/PopCommerceAdmin.QuickSearch`: General order and customer search |
| 55 | 55 | ||
| 56 | ### Customer Management | 56 | ### Customer Management |
| 57 | - `PopCommerce.PopCommerceRoot.Customer`: Manage customer accounts | 57 | - `/PopCommerce/PopCommerceRoot.Customer`: Manage customer accounts |
| 58 | - `PopCommerce.PopCommerceAdmin.QuickSearch`: Customer lookup | 58 | - `/PopCommerce/PopCommerceAdmin.QuickSearch`: Customer lookup |
| 59 | 59 | ||
| 60 | ## Tips for LLM Clients | 60 | ## Tips for LLM Clients |
| 61 | 61 | ||
| 62 | - All screens support parameterized queries for filtering results | 62 | - All screens support parameterized queries for filtering results |
| 63 | - Use `moqui_render_screen` with screen path to execute screens | 63 | - Use `moqui_render_screen` with screen path to execute screens |
| 64 | - Screen paths use dot notation (e.g., `PopCommerce.Catalog.Product`) | 64 | - Screen paths use dot notation (e.g., `/PopCommerce/Catalog.Product`) |
| 65 | - Check `moqui_get_screen_details` for required parameters before rendering | 65 | - Check `moqui_get_screen_details` for required parameters before rendering |
| 66 | - Use `renderMode: "mcp"` for structured JSON output or `"text"` for human-readable format]]></fileData> | 66 | - Use `renderMode: "mcp"` for structured JSON output or `"text"` for human-readable format]]></fileData> |
| 67 | </moqui.resource.DbResourceFile> | 67 | </moqui.resource.DbResourceFile> |
| ... | @@ -107,23 +107,23 @@ Use the following discovery tools to explore available functionality: | ... | @@ -107,23 +107,23 @@ Use the following discovery tools to explore available functionality: |
| 107 | ## Common Screen Paths | 107 | ## Common Screen Paths |
| 108 | 108 | ||
| 109 | ### Catalog Operations | 109 | ### Catalog Operations |
| 110 | - `PopCommerce.PopCommerceAdmin.Catalog.Product.FindProduct`: Search and browse products | 110 | - `/PopCommerce/PopCommerceAdmin.Catalog.Product.FindProduct`: Search and browse products |
| 111 | - `PopCommerce.PopCommerceAdmin.Catalog.Feature.FindFeature`: Search by features like color or size | 111 | - `/PopCommerce/PopCommerceAdmin.Catalog.Feature.FindFeature`: Search by features like color or size |
| 112 | - `PopCommerce.PopCommerceAdmin.Catalog.Product.EditPrices`: View and update product prices | 112 | - `/PopCommerce/PopCommerceAdmin.Catalog.Product.EditPrices`: View and update product prices |
| 113 | 113 | ||
| 114 | ### Order Management | 114 | ### Order Management |
| 115 | - `PopCommerce.PopCommerceAdmin.Order.FindOrder`: Lookup order status and details | 115 | - `/PopCommerce/PopCommerceAdmin.Order.FindOrder`: Lookup order status and details |
| 116 | - `PopCommerce.PopCommerceAdmin.QuickSearch`: General order and customer search | 116 | - `/PopCommerce/PopCommerceAdmin.QuickSearch`: General order and customer search |
| 117 | 117 | ||
| 118 | ### Customer Management | 118 | ### Customer Management |
| 119 | - `PopCommerce.PopCommerceRoot.Customer`: Manage customer accounts | 119 | - `/PopCommerce/PopCommerceRoot.Customer`: Manage customer accounts |
| 120 | - `PopCommerce.PopCommerceAdmin.QuickSearch`: Customer lookup | 120 | - `/PopCommerce/PopCommerceAdmin.QuickSearch`: Customer lookup |
| 121 | 121 | ||
| 122 | ## Tips for LLM Clients | 122 | ## Tips for LLM Clients |
| 123 | 123 | ||
| 124 | - All screens support parameterized queries for filtering results | 124 | - All screens support parameterized queries for filtering results |
| 125 | - Use `moqui_render_screen` with screen path to execute screens | 125 | - Use `moqui_render_screen` with screen path to execute screens |
| 126 | - Screen paths use dot notation (e.g., `PopCommerce.Catalog.Product`) | 126 | - Screen paths use dot notation (e.g., `/PopCommerce/Catalog.Product`) |
| 127 | - Check `moqui_get_screen_details` for required parameters before rendering | 127 | - Check `moqui_get_screen_details` for required parameters before rendering |
| 128 | - Use `renderMode: "mcp"` for structured JSON output or `"text"` for human-readable format]]></fileData> | 128 | - Use `renderMode: "mcp"` for structured JSON output or `"text"` for human-readable format]]></fileData> |
| 129 | </moqui.resource.DbResourceFile> | 129 | </moqui.resource.DbResourceFile> |
| ... | @@ -132,7 +132,7 @@ Use the following discovery tools to explore available functionality: | ... | @@ -132,7 +132,7 @@ Use the following discovery tools to explore available functionality: |
| 132 | <moqui.resource.DbResource | 132 | <moqui.resource.DbResource |
| 133 | resourceId="WIKI_MCP_DOCS_POPCOMM_ROOT" | 133 | resourceId="WIKI_MCP_DOCS_POPCOMM_ROOT" |
| 134 | parentResourceId="WIKI_MCP_SCREEN_DOCS" | 134 | parentResourceId="WIKI_MCP_SCREEN_DOCS" |
| 135 | filename="PopCommerce.PopCommerceRoot.md" | 135 | filename="PopCommerce/PopCommerceRoot.md" |
| 136 | isFile="Y"/> | 136 | isFile="Y"/> |
| 137 | <moqui.resource.DbResourceFile | 137 | <moqui.resource.DbResourceFile |
| 138 | resourceId="WIKI_MCP_DOCS_POPCOMM_ROOT" | 138 | resourceId="WIKI_MCP_DOCS_POPCOMM_ROOT" |
| ... | @@ -158,7 +158,7 @@ Use browse tools to explore the full catalog of PopCommerce screens starting fro | ... | @@ -158,7 +158,7 @@ Use browse tools to explore the full catalog of PopCommerce screens starting fro |
| 158 | <moqui.resource.wiki.WikiPage | 158 | <moqui.resource.wiki.WikiPage |
| 159 | wikiPageId="MCP_SCREEN_DOCS/PopCommerceRoot" | 159 | wikiPageId="MCP_SCREEN_DOCS/PopCommerceRoot" |
| 160 | wikiSpaceId="MCP_SCREEN_DOCS" | 160 | wikiSpaceId="MCP_SCREEN_DOCS" |
| 161 | pagePath="PopCommerce.PopCommerceRoot" | 161 | pagePath="PopCommerce/PopCommerceRoot" |
| 162 | publishedVersionName="v1" | 162 | publishedVersionName="v1" |
| 163 | restrictView="N"> | 163 | restrictView="N"> |
| 164 | </moqui.resource.wiki.WikiPage> | 164 | </moqui.resource.wiki.WikiPage> | ... | ... |
| ... | @@ -7,7 +7,9 @@ | ... | @@ -7,7 +7,9 @@ |
| 7 | 7 | ||
| 8 | <#macro @element></#macro> | 8 | <#macro @element></#macro> |
| 9 | 9 | ||
| 10 | <#macro screen><#recurse></#macro> | 10 | <#macro screen> |
| 11 | <#recurse> | ||
| 12 | </#macro> | ||
| 11 | 13 | ||
| 12 | <#macro widgets> | 14 | <#macro widgets> |
| 13 | <#recurse> | 15 | <#recurse> |
| ... | @@ -48,6 +50,7 @@ | ... | @@ -48,6 +50,7 @@ |
| 48 | 50 | ||
| 49 | <#macro "container-dialog"> | 51 | <#macro "container-dialog"> |
| 50 | [Button: ${ec.resource.expand(.node["@button-text"], "")}] | 52 | [Button: ${ec.resource.expand(.node["@button-text"], "")}] |
| 53 | <#recurse> | ||
| 51 | </#macro> | 54 | </#macro> |
| 52 | 55 | ||
| 53 | <#-- ================== Standalone Fields ==================== --> | 56 | <#-- ================== Standalone Fields ==================== --> |
| ... | @@ -66,18 +69,18 @@ | ... | @@ -66,18 +69,18 @@ |
| 66 | <#assign linkText = sri.getFieldValueString(.node?parent?parent)> | 69 | <#assign linkText = sri.getFieldValueString(.node?parent?parent)> |
| 67 | </#if> | 70 | </#if> |
| 68 | 71 | ||
| 69 | <#-- Convert path to dot notation for moqui_render_screen --> | 72 | <#-- Convert path to slash notation for moqui_render_screen (matches browser URLs) --> |
| 70 | <#assign fullPath = urlInstance.sui.fullPathNameList![]> | 73 | <#assign fullPath = urlInstance.sui.fullPathNameList![]> |
| 71 | <#assign dotPath = ""> | 74 | <#assign slashPath = ""> |
| 72 | <#list fullPath as pathPart><#assign dotPath = dotPath + (dotPath?has_content)?then(".", "") + pathPart></#list> | 75 | <#list fullPath as pathPart><#assign slashPath = slashPath + (slashPath?has_content)?then("/", "") + pathPart></#list> |
| 73 | 76 | ||
| 74 | <#assign paramStr = urlInstance.getParameterString()> | 77 | <#assign paramStr = urlInstance.getParameterString()> |
| 75 | <#if paramStr?has_content><#assign dotPath = dotPath + "?" + paramStr></#if> | 78 | <#if paramStr?has_content><#assign slashPath = slashPath + "?" + paramStr></#if> |
| 76 | 79 | ||
| 77 | [${linkText}](${dotPath})<#t> | 80 | [${linkText}](${slashPath})<#t> |
| 78 | <#if mcpSemanticData??> | 81 | <#if mcpSemanticData??> |
| 79 | <#if !mcpSemanticData.links??><#assign dummy = mcpSemanticData.put("links", [])></#if> | 82 | <#if !mcpSemanticData.links??><#assign dummy = mcpSemanticData.put("links", [])></#if> |
| 80 | <#assign linkInfo = {"text": linkText, "path": dotPath, "type": "navigation"}> | 83 | <#assign linkInfo = {"text": linkText, "path": slashPath, "type": "navigation"}> |
| 81 | <#assign dummy = mcpSemanticData.links.add(linkInfo)> | 84 | <#assign dummy = mcpSemanticData.links.add(linkInfo)> |
| 82 | </#if> | 85 | </#if> |
| 83 | </#if> | 86 | </#if> |
| ... | @@ -98,17 +101,16 @@ | ... | @@ -98,17 +101,16 @@ |
| 98 | <#-- ======================= Form ========================= --> | 101 | <#-- ======================= Form ========================= --> |
| 99 | <#macro "form-single"> | 102 | <#macro "form-single"> |
| 100 | <#assign formNode = sri.getFormNode(.node["@name"])> | 103 | <#assign formNode = sri.getFormNode(.node["@name"])> |
| 101 | <#assign mapName = formNode["@map"]!"fieldValues"> | 104 | <#assign mapName = (formNode["@map"]!"fieldValues")> |
| 102 | <#assign formMap = ec.resource.expression(mapName, "")!> | 105 | <#assign formMap = ec.resource.expression(mapName, "")!> |
| 103 | 106 | ||
| 104 | <#if mcpSemanticData??> | 107 | <#if mcpSemanticData??> |
| 105 | <#if !mcpSemanticData.formMetadata??><#assign dummy = mcpSemanticData.put("formMetadata", {})</#if> | 108 | <#if !mcpSemanticData.formMetadata??><#assign dummy = mcpSemanticData.put("formMetadata", {})></#if> |
| 106 | 109 | ||
| 107 | <#assign formMeta = {}> | 110 | <#assign formMeta = {"name": (.node["@name"]!""), "map": mapName}> |
| 108 | <#assign formMeta = formMeta + {"name": .node["@name"]!"", "map": mapName}> | ||
| 109 | <#assign fieldMetaList = []> | 111 | <#assign fieldMetaList = []> |
| 110 | 112 | ||
| 111 | <#assign dummy = mcpSemanticData.formMeta.put(.node["@name"], formMeta)> | 113 | <#assign dummy = mcpSemanticData.formMetadata.put(.node["@name"], formMeta)> |
| 112 | </#if> | 114 | </#if> |
| 113 | 115 | ||
| 114 | <#if mcpSemanticData?? && formMap?has_content><#assign dummy = mcpSemanticData.put(.node["@name"], formMap)></#if> | 116 | <#if mcpSemanticData?? && formMap?has_content><#assign dummy = mcpSemanticData.put(.node["@name"], formMap)></#if> |
| ... | @@ -121,8 +123,7 @@ | ... | @@ -121,8 +123,7 @@ |
| 121 | <#assign title><@fieldTitle fieldSubNode/></#assign> | 123 | <#assign title><@fieldTitle fieldSubNode/></#assign> |
| 122 | 124 | ||
| 123 | <#if mcpSemanticData??> | 125 | <#if mcpSemanticData??> |
| 124 | <#assign fieldMeta = {}> | 126 | <#assign fieldMeta = {"name": (fieldNode["@name"]!""), "title": (title!), "required": (fieldNode["@required"]! == "true")}> |
| 125 | <#assign fieldMeta = fieldMeta + {"name": fieldNode["@name"]!"", "title": title!"", "required": (fieldNode["@required"]! == "true")}> | ||
| 126 | 127 | ||
| 127 | <#if fieldSubNode["text-line"]?has_content><#assign dummy = fieldMeta.put("type", "text")></#if> | 128 | <#if fieldSubNode["text-line"]?has_content><#assign dummy = fieldMeta.put("type", "text")></#if> |
| 128 | <#if fieldSubNode["text-area"]?has_content><#assign dummy = fieldMeta.put("type", "textarea")></#if> | 129 | <#if fieldSubNode["text-area"]?has_content><#assign dummy = fieldMeta.put("type", "textarea")></#if> |
| ... | @@ -138,7 +139,7 @@ | ... | @@ -138,7 +139,7 @@ |
| 138 | </#list> | 139 | </#list> |
| 139 | 140 | ||
| 140 | <#if mcpSemanticData?? && fieldMetaList?has_content> | 141 | <#if mcpSemanticData?? && fieldMetaList?has_content> |
| 141 | <#assign dummy = mcpSemanticData.formMeta[.node["@name"]!].put("fields", fieldMetaList)> | 142 | <#assign dummy = mcpSemanticData.formMetadata[.node["@name"]!].put("fields", fieldMetaList)> |
| 142 | </#if> | 143 | </#if> |
| 143 | 144 | ||
| 144 | <#t>${sri.popContext()} | 145 | <#t>${sri.popContext()} |
| ... | @@ -154,12 +155,9 @@ | ... | @@ -154,12 +155,9 @@ |
| 154 | 155 | ||
| 155 | <#if mcpSemanticData?? && listObject?has_content> | 156 | <#if mcpSemanticData?? && listObject?has_content> |
| 156 | <#assign truncatedList = listObject> | 157 | <#assign truncatedList = listObject> |
| 157 | <#if listObject?size > 50> | ||
| 158 | <#assign truncatedList = listObject?take(50)> | ||
| 159 | </#if> | ||
| 160 | <#assign dummy = mcpSemanticData.put(.node["@name"], truncatedList)> | 158 | <#assign dummy = mcpSemanticData.put(.node["@name"], truncatedList)> |
| 161 | 159 | ||
| 162 | <#if !mcpSemanticData.listMetadata??><#assign dummy = mcpSemanticData.put("listMetadata", {})</#if> | 160 | <#if !mcpSemanticData.listMetadata??><#assign dummy = mcpSemanticData.put("listMetadata", {})></#if> |
| 163 | 161 | ||
| 164 | <#assign columnNames = []> | 162 | <#assign columnNames = []> |
| 165 | <#list formListColumnList as columnFieldList> | 163 | <#list formListColumnList as columnFieldList> |
| ... | @@ -167,11 +165,11 @@ | ... | @@ -167,11 +165,11 @@ |
| 167 | <#assign dummy = columnNames.add(fieldNode["@name"]!"")> | 165 | <#assign dummy = columnNames.add(fieldNode["@name"]!"")> |
| 168 | </#list> | 166 | </#list> |
| 169 | 167 | ||
| 170 | <#assign dummy = mcpSemanticData.listMeta.put(.node["@name"]!"", { | 168 | <#assign dummy = mcpSemanticData.listMetadata.put(.node["@name"]!"", { |
| 171 | "name": .node["@name"]!"", | 169 | "name": .node["@name"]!"", |
| 172 | "totalItems": totalItems, | 170 | "totalItems": totalItems, |
| 173 | "displayedItems": truncatedList?size, | 171 | "displayedItems": (totalItems > 50)?then(50, totalItems), |
| 174 | "truncated": (listObject?size > 50), | 172 | "truncated": (totalItems > 50), |
| 175 | "columns": columnNames | 173 | "columns": columnNames |
| 176 | })> | 174 | })> |
| 177 | </#if> | 175 | </#if> |
| ... | @@ -185,7 +183,8 @@ | ... | @@ -185,7 +183,8 @@ |
| 185 | | | 183 | | |
| 186 | <#list formListColumnList as columnFieldList>| --- </#list>| | 184 | <#list formListColumnList as columnFieldList>| --- </#list>| |
| 187 | <#-- Data Rows --> | 185 | <#-- Data Rows --> |
| 188 | <#list (truncatedList?? && truncatedList?size > 0)!listObject as listEntry> | 186 | <#list listObject as listEntry> |
| 187 | <#if (listEntry_index >= 50)><#break></#if> | ||
| 189 | <#t>${sri.startFormListRow(formListInfo, listEntry, listEntry_index, listEntry_has_next)} | 188 | <#t>${sri.startFormListRow(formListInfo, listEntry, listEntry_index, listEntry_has_next)} |
| 190 | <#list formListColumnList as columnFieldList> | 189 | <#list formListColumnList as columnFieldList> |
| 191 | <#t>| <#list columnFieldList as fieldNode><@formListSubField fieldNode/><#if fieldNode_has_next> </#if></#list><#t> | 190 | <#t>| <#list columnFieldList as fieldNode><@formListSubField fieldNode/><#if fieldNode_has_next> </#if></#list><#t> | ... | ... |
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!-- | ||
| 3 | This software is in the public domain under CC0 1.0 Universal plus a | ||
| 4 | Grant of Patent License. | ||
| 5 | |||
| 6 | To the extent possible under law, author(s) have dedicated all | ||
| 7 | copyright and related and neighboring rights to this software to the | ||
| 8 | public domain worldwide. This software is distributed without any | ||
| 9 | warranty. | ||
| 10 | --> | ||
| 11 | <services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-3.xsd"> | ||
| 12 | |||
| 13 | <service verb="mcp" noun="ExecuteScreenAction" authenticate="true" allow-remote="true" transaction-timeout="60"> | ||
| 14 | <description>Execute a screen action (transition or CRUD convention) with parameters.</description> | ||
| 15 | <in-parameters> | ||
| 16 | <parameter name="path" required="true"/> | ||
| 17 | <parameter name="action" required="true"/> | ||
| 18 | <parameter name="parameters" type="Map"/> | ||
| 19 | </in-parameters> | ||
| 20 | <out-parameters> | ||
| 21 | <parameter name="result" type="Map"/> | ||
| 22 | </out-parameters> | ||
| 23 | <actions> | ||
| 24 | <script><![CDATA[ | ||
| 25 | import org.moqui.context.ExecutionContext | ||
| 26 | |||
| 27 | ExecutionContext ec = context.ec | ||
| 28 | def actionResult = null | ||
| 29 | def actionError = null | ||
| 30 | def actionParams = parameters ?: [:] | ||
| 31 | |||
| 32 | try { | ||
| 33 | // First, resolve the screen path | ||
| 34 | def resolveResult = ec.service.sync().name("McpServices.mcp#ResolveScreenPath") | ||
| 35 | .parameter("path", path).call() | ||
| 36 | |||
| 37 | def screenPath = resolveResult.screenPath | ||
| 38 | def screenDef = resolveResult.screenDef | ||
| 39 | |||
| 40 | if (!screenDef) { | ||
| 41 | actionError = "Could not resolve screen for path '${path}'" | ||
| 42 | result = [actionResult: actionResult, actionError: actionError] | ||
| 43 | return | ||
| 44 | } | ||
| 45 | |||
| 46 | if (action == "submit") { | ||
| 47 | // Special handling for form submit - just acknowledge receipt | ||
| 48 | actionResult = [ | ||
| 49 | action: "submit", | ||
| 50 | status: "success", | ||
| 51 | message: "Form parameters submitted", | ||
| 52 | parametersProcessed: actionParams.keySet() | ||
| 53 | ] | ||
| 54 | } else { | ||
| 55 | // Check if action matches CRUD convention for ProductPrice | ||
| 56 | def actionPrefix = action?.take(6) | ||
| 57 | if (actionPrefix && actionPrefix in ['update', 'create', 'delete']) { | ||
| 58 | // Convention: updateProductPrice -> update#mantle.product.ProductPrice | ||
| 59 | def serviceName = "${actionPrefix}#mantle.product.ProductPrice" | ||
| 60 | ec.logger.info("ExecuteScreenAction: Calling service by convention: ${serviceName}") | ||
| 61 | |||
| 62 | try { | ||
| 63 | def serviceCallResult = ec.service.sync().name(serviceName).parameters(actionParams).call() | ||
| 64 | actionResult = [ | ||
| 65 | action: action, | ||
| 66 | status: "executed", | ||
| 67 | message: "Executed service ${serviceName}", | ||
| 68 | result: serviceCallResult | ||
| 69 | ] | ||
| 70 | } catch (Exception e) { | ||
| 71 | actionError = "Service call failed: ${e.message}" | ||
| 72 | } | ||
| 73 | } else { | ||
| 74 | // Look for matching transition on screen | ||
| 75 | def foundTransition = null | ||
| 76 | def allTransitions = screenDef.getAllTransitions() | ||
| 77 | if (allTransitions) { | ||
| 78 | for (def transition : allTransitions) { | ||
| 79 | if (transition.getName() == action) { | ||
| 80 | foundTransition = transition | ||
| 81 | break | ||
| 82 | } | ||
| 83 | } | ||
| 84 | } | ||
| 85 | |||
| 86 | if (foundTransition) { | ||
| 87 | // Try to find and execute the transition's service | ||
| 88 | def serviceName = null | ||
| 89 | if (foundTransition.xmlTransition) { | ||
| 90 | def serviceCallNode = foundTransition.xmlTransition.first("service-call") | ||
| 91 | if (serviceCallNode) serviceName = serviceCallNode.attribute("name") | ||
| 92 | } | ||
| 93 | |||
| 94 | if (serviceName) { | ||
| 95 | ec.logger.info("ExecuteScreenAction: Executing transition '${action}' service: ${serviceName}") | ||
| 96 | try { | ||
| 97 | def serviceCallResult = ec.service.sync().name(serviceName).parameters(actionParams).call() | ||
| 98 | actionResult = [ | ||
| 99 | action: action, | ||
| 100 | status: "executed", | ||
| 101 | message: "Executed service ${serviceName}", | ||
| 102 | result: serviceCallResult | ||
| 103 | ] | ||
| 104 | } catch (Exception e) { | ||
| 105 | actionError = "Service call failed: ${e.message}" | ||
| 106 | } | ||
| 107 | } else { | ||
| 108 | actionResult = [ | ||
| 109 | action: action, | ||
| 110 | status: "success", | ||
| 111 | message: "Transition '${action}' ready (no direct service)" | ||
| 112 | ] | ||
| 113 | } | ||
| 114 | } else { | ||
| 115 | actionError = "Transition '${action}' not found on screen" | ||
| 116 | } | ||
| 117 | } | ||
| 118 | } | ||
| 119 | } catch (Exception e) { | ||
| 120 | actionError = "Action execution failed: ${e.message}" | ||
| 121 | ec.logger.warn("ExecuteScreenAction error: ${e.message}") | ||
| 122 | } | ||
| 123 | |||
| 124 | result = [actionResult: actionResult, actionError: actionError] | ||
| 125 | ]]></script> | ||
| 126 | </actions> | ||
| 127 | </service> | ||
| 128 | |||
| 129 | </services> |
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!-- | ||
| 3 | This software is in the public domain under CC0 1.0 Universal plus a | ||
| 4 | Grant of Patent License. | ||
| 5 | |||
| 6 | To the extent possible under law, author(s) have dedicated all | ||
| 7 | copyright and related and neighboring rights to this software to the | ||
| 8 | public domain worldwide. This software is distributed without any | ||
| 9 | warranty. | ||
| 10 | --> | ||
| 11 | <services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-3.xsd"> | ||
| 12 | |||
| 13 | <service verb="mcp" noun="RenderScreenNarrative" authenticate="true" allow-remote="true" transaction-timeout="120"> | ||
| 14 | <description>Render a screen with semantic state extraction and UI narrative generation.</description> | ||
| 15 | <in-parameters> | ||
| 16 | <parameter name="path" required="true"/> | ||
| 17 | <parameter name="parameters" type="Map"/> | ||
| 18 | <parameter name="renderMode" default="mcp"/> | ||
| 19 | <parameter name="sessionId"/> | ||
| 20 | <parameter name="terse" type="Boolean" default="false"/> | ||
| 21 | </in-parameters> | ||
| 22 | <out-parameters> | ||
| 23 | <parameter name="result" type="Map"/> | ||
| 24 | </out-parameters> | ||
| 25 | <actions> | ||
| 26 | <script><![CDATA[ | ||
| 27 | import org.moqui.context.ExecutionContext | ||
| 28 | |||
| 29 | ExecutionContext ec = context.ec | ||
| 30 | def renderedContent = null | ||
| 31 | def semanticState = null | ||
| 32 | def uiNarrative = null | ||
| 33 | |||
| 34 | // Resolve screen path | ||
| 35 | def resolveResult = ec.service.sync().name("McpServices.mcp#ResolveScreenPath") | ||
| 36 | .parameter("path", path).call() | ||
| 37 | |||
| 38 | def screenPath = resolveResult.screenPath | ||
| 39 | def subscreenName = resolveResult.subscreenName | ||
| 40 | |||
| 41 | if (!screenPath) { | ||
| 42 | result = [renderedContent: null, semanticState: null, uiNarrative: null] | ||
| 43 | return | ||
| 44 | } | ||
| 45 | |||
| 46 | // Build render parameters | ||
| 47 | def screenCallParams = [ | ||
| 48 | path: screenPath, | ||
| 49 | parameters: parameters ?: [:], | ||
| 50 | renderMode: renderMode ?: "mcp", | ||
| 51 | sessionId: sessionId, | ||
| 52 | terse: terse == true | ||
| 53 | ] | ||
| 54 | if (subscreenName) screenCallParams.subscreenName = subscreenName | ||
| 55 | |||
| 56 | // Render screen using ScreenAsMcpTool | ||
| 57 | try { | ||
| 58 | def serviceResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool") | ||
| 59 | .parameters(screenCallParams).call() | ||
| 60 | |||
| 61 | // Extract semantic state from rendered result | ||
| 62 | if (serviceResult) { | ||
| 63 | def resultObj = null | ||
| 64 | if (serviceResult.containsKey('content') && serviceResult.content && serviceResult.content.size() > 0) { | ||
| 65 | def rawText = serviceResult.content[0].text | ||
| 66 | if (rawText && rawText.startsWith("{")) { | ||
| 67 | try { resultObj = new groovy.json.JsonSlurper().parseText(rawText) } catch(e) {} | ||
| 68 | } | ||
| 69 | renderedContent = rawText | ||
| 70 | } else if (serviceResult.containsKey('result') && serviceResult.result && serviceResult.result.content && serviceResult.result.content.size() > 0) { | ||
| 71 | def rawText = serviceResult.result.content[0].text | ||
| 72 | if (rawText && rawText.startsWith("{")) { | ||
| 73 | try { resultObj = new groovy.json.JsonSlurper().parseText(rawText) } catch(e) {} | ||
| 74 | } | ||
| 75 | renderedContent = rawText | ||
| 76 | } | ||
| 77 | |||
| 78 | // Generate UI narrative if we have semantic state | ||
| 79 | if (resultObj && resultObj.semanticState) { | ||
| 80 | semanticState = resultObj.semanticState | ||
| 81 | |||
| 82 | try { | ||
| 83 | def narrativeBuilder = new org.moqui.mcp.UiNarrativeBuilder() | ||
| 84 | def screenDefForNarrative = ec.screen.getScreenDefinition(screenPath) | ||
| 85 | |||
| 86 | uiNarrative = narrativeBuilder.buildNarrative( | ||
| 87 | screenDefForNarrative, | ||
| 88 | semanticState, | ||
| 89 | path, | ||
| 90 | terse == true | ||
| 91 | ) | ||
| 92 | ec.logger.info("RenderScreenNarrative: Generated UI narrative for ${path}") | ||
| 93 | } catch (Exception e) { | ||
| 94 | ec.logger.warn("RenderScreenNarrative: Failed to generate UI narrative: ${e.message}") | ||
| 95 | } | ||
| 96 | |||
| 97 | // Truncate content if we have UI narrative to save tokens | ||
| 98 | if (renderedContent && renderedContent.length() > 500) { | ||
| 99 | renderedContent = renderedContent.take(500) + "... (truncated, see uiNarrative for actions)" | ||
| 100 | } | ||
| 101 | } | ||
| 102 | } | ||
| 103 | } catch (Exception e) { | ||
| 104 | ec.logger.warn("RenderScreenNarrative: Error rendering screen ${path}: ${e.message}") | ||
| 105 | renderedContent = "RENDER_ERROR: ${e.message}" | ||
| 106 | } | ||
| 107 | |||
| 108 | result = [ | ||
| 109 | renderedContent: renderedContent, | ||
| 110 | semanticState: semanticState, | ||
| 111 | uiNarrative: uiNarrative | ||
| 112 | ] | ||
| 113 | ]]></script> | ||
| 114 | </actions> | ||
| 115 | </service> | ||
| 116 | |||
| 117 | </services> |
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!-- | ||
| 3 | This software is in the public domain under CC0 1.0 Universal plus a | ||
| 4 | Grant of Patent License. | ||
| 5 | |||
| 6 | To the extent possible under law, author(s) have dedicated all | ||
| 7 | copyright and related and neighboring rights to this software to the | ||
| 8 | public domain worldwide. This software is distributed without any | ||
| 9 | warranty. | ||
| 10 | --> | ||
| 11 | <services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-3.xsd"> | ||
| 12 | |||
| 13 | <service verb="mcp" noun="ResolveScreenPath" authenticate="false" allow-remote="true" transaction-timeout="30"> | ||
| 14 | <description>Resolve a simple screen path to component path, subscreen name, and screen definition.</description> | ||
| 15 | <in-parameters> | ||
| 16 | <parameter name="path" required="true"/> | ||
| 17 | </in-parameters> | ||
| 18 | <out-parameters> | ||
| 19 | <parameter name="result" type="Map"/> | ||
| 20 | </out-parameters> | ||
| 21 | <actions> | ||
| 22 | <script><![CDATA[ | ||
| 23 | import org.moqui.context.ExecutionContext | ||
| 24 | |||
| 25 | ExecutionContext ec = context.ec | ||
| 26 | def resolvedPath = null | ||
| 27 | def subscreenName = null | ||
| 28 | def screenDef = null | ||
| 29 | |||
| 30 | if (!path || path == "root") { | ||
| 31 | return [ | ||
| 32 | screenPath: null, | ||
| 33 | subscreenName: null, | ||
| 34 | screenDef: null | ||
| 35 | ] | ||
| 36 | } | ||
| 37 | |||
| 38 | // Support both dot and slash notation | ||
| 39 | def pathParts = path.contains('/') | ||
| 40 | ? path.split('/') | ||
| 41 | : path.split('\\.') | ||
| 42 | def componentName = pathParts[0] | ||
| 43 | |||
| 44 | // Use longest prefix match to find actual screen file | ||
| 45 | for (int i = pathParts.size(); i >= 1; i--) { | ||
| 46 | def subPath = i > 1 ? pathParts[0] + "/" + (pathParts[1..<i].join('/')) : pathParts[0] | ||
| 47 | def currentTry = "component://${componentName}/screen/${subPath}.xml" | ||
| 48 | if (ec.resource.getLocationReference(currentTry).getExists()) { | ||
| 49 | resolvedPath = currentTry | ||
| 50 | // If we found a screen matching the full path, we're already at the target | ||
| 51 | if (i < pathParts.size()) { | ||
| 52 | def remainingParts = pathParts[i..-1] | ||
| 53 | subscreenName = remainingParts.size() > 1 ? remainingParts.join('_') : remainingParts[0] | ||
| 54 | } | ||
| 55 | break | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | // Fallback to component root screen if no match found | ||
| 60 | if (!resolvedPath) { | ||
| 61 | resolvedPath = "component://${componentName}/screen/${componentName}.xml" | ||
| 62 | if (pathParts.size() > 1) { | ||
| 63 | subscreenName = pathParts[1..-1].join('_') | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | // Navigate to subscreen if needed | ||
| 68 | if (resolvedPath) { | ||
| 69 | try { | ||
| 70 | screenDef = ec.screen.getScreenDefinition(resolvedPath) | ||
| 71 | if (subscreenName && screenDef) { | ||
| 72 | for (subName in subscreenName.split('_')) { | ||
| 73 | def subItem = screenDef?.getSubscreensItem(subName) | ||
| 74 | if (subItem && subItem.getLocation()) { | ||
| 75 | screenDef = ec.screen.getScreenDefinition(subItem.getLocation()) | ||
| 76 | } else { | ||
| 77 | break | ||
| 78 | } | ||
| 79 | } | ||
| 80 | } | ||
| 81 | } catch (Exception e) { | ||
| 82 | ec.logger.warn("ResolveScreenPath: Error loading screen definition: ${e.message}") | ||
| 83 | } | ||
| 84 | } | ||
| 85 | |||
| 86 | result = [ | ||
| 87 | screenPath: resolvedPath, | ||
| 88 | subscreenName: subscreenName, | ||
| 89 | screenDef: screenDef | ||
| 90 | ] | ||
| 91 | ]]></script> | ||
| 92 | </actions> | ||
| 93 | </service> | ||
| 94 | |||
| 95 | </services> |
This diff is collapsed.
Click to expand it.
| ... | @@ -93,13 +93,26 @@ class CustomScreenTestImpl implements McpScreenTest { | ... | @@ -93,13 +93,26 @@ class CustomScreenTestImpl implements McpScreenTest { |
| 93 | return this | 93 | return this |
| 94 | } | 94 | } |
| 95 | 95 | ||
| 96 | protected static List<String> pathToList(String path) { | ||
| 97 | List<String> pathList = new ArrayList<>() | ||
| 98 | if (path && path.contains('/')) { | ||
| 99 | String[] pathSegments = path.split('/') | ||
| 100 | for (String segment in pathSegments) { | ||
| 101 | if (segment && segment.trim().length() > 0) { | ||
| 102 | pathList.add(segment) | ||
| 103 | } | ||
| 104 | } | ||
| 105 | } | ||
| 106 | return pathList | ||
| 107 | } | ||
| 108 | |||
| 96 | @Override | 109 | @Override |
| 97 | McpScreenTest baseScreenPath(String screenPath) { | 110 | McpScreenTest baseScreenPath(String screenPath) { |
| 98 | if (!rootScreenLocation) throw new BaseArtifactException("No rootScreen specified") | 111 | if (!rootScreenLocation) throw new BaseArtifactException("No rootScreen specified") |
| 99 | baseScreenPath = screenPath | 112 | baseScreenPath = screenPath |
| 100 | if (baseScreenPath.endsWith("/")) baseScreenPath = baseScreenPath.substring(0, baseScreenPath.length() - 1) | 113 | if (baseScreenPath.endsWith("/")) baseScreenPath = baseScreenPath.substring(0, baseScreenPath.length() - 1) |
| 101 | if (baseScreenPath) { | 114 | if (baseScreenPath) { |
| 102 | baseScreenPathList = ScreenUrlInfo.parseSubScreenPath(rootScreenDef, rootScreenDef, [], baseScreenPath, null, sfi) | 115 | baseScreenPathList = ScreenUrlInfo.parseSubScreenPath(rootScreenDef, rootScreenDef, pathToList(baseScreenPath), baseScreenPath, [:], sfi) |
| 103 | if (baseScreenPathList == null) throw new BaseArtifactException("Error in baseScreenPath, could find not base screen path ${baseScreenPath} under ${rootScreenDef.location}") | 116 | if (baseScreenPathList == null) throw new BaseArtifactException("Error in baseScreenPath, could find not base screen path ${baseScreenPath} under ${rootScreenDef.location}") |
| 104 | for (String screenName in baseScreenPathList) { | 117 | for (String screenName in baseScreenPathList) { |
| 105 | ScreenDefinition.SubscreensItem ssi = baseScreenDef.getSubscreensItem(screenName) | 118 | ScreenDefinition.SubscreensItem ssi = baseScreenDef.getSubscreensItem(screenName) |
| ... | @@ -282,10 +295,24 @@ class CustomScreenTestImpl implements McpScreenTest { | ... | @@ -282,10 +295,24 @@ class CustomScreenTestImpl implements McpScreenTest { |
| 282 | } | 295 | } |
| 283 | } | 296 | } |
| 284 | logger.info("Custom screen path parsing for non-webroot root: ${screenPathList}") | 297 | logger.info("Custom screen path parsing for non-webroot root: ${screenPathList}") |
| 285 | } else { | 298 | } else { |
| 286 | screenPathList = ScreenUrlInfo.parseSubScreenPath(csti.rootScreenDef, csti.baseScreenDef, | 299 | // For webroot or other cases, use ScreenUrlInfo.parseSubScreenPath for resolution |
| 287 | csti.baseScreenPathList, stri.screenPath, stri.parameters, csti.sfi) | 300 | // Convert screenPath to list for parseSubScreenPath |
| 288 | } | 301 | List<String> inputPathList = new ArrayList<>() |
| 302 | if (stri.screenPath && stri.screenPath.contains('/')) { | ||
| 303 | String[] pathSegments = stri.screenPath.split('/') | ||
| 304 | for (String segment in pathSegments) { | ||
| 305 | if (segment && segment.trim().length() > 0) { | ||
| 306 | inputPathList.add(segment) | ||
| 307 | } | ||
| 308 | } | ||
| 309 | } | ||
| 310 | |||
| 311 | // Use Moqui's parseSubScreenPath to resolve actual screen path | ||
| 312 | // Note: pass null for fromPathList since stri.screenPath is already relative to root or from screen | ||
| 313 | screenPathList = ScreenUrlInfo.parseSubScreenPath(csti.rootScreenDef, csti.baseScreenDef, | ||
| 314 | null, stri.screenPath, stri.parameters, csti.sfi) | ||
| 315 | } | ||
| 289 | if (screenPathList == null) throw new BaseArtifactException("Could not find screen path ${stri.screenPath} under base screen ${csti.baseScreenDef.location}") | 316 | if (screenPathList == null) throw new BaseArtifactException("Could not find screen path ${stri.screenPath} under base screen ${csti.baseScreenDef.location}") |
| 290 | 317 | ||
| 291 | // push the context | 318 | // push the context | ... | ... |
-
Please register or sign in to post a comment