87e0b6a4 by Ean Schuessler

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
1 parent 09883cfe
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>
...@@ -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
......