a06fd54e by Ean Schuessler

Implement universal autocomplete with transition metadata extraction

- Extract service name and in-map from transition XML
- Capture depends-on parameter attribute for field name mapping
- Parse in-map parameter mapping for proper service calls
- Support server-search autocomplete with term parameter
- Fallback to ScreenAsMcpTool for entity-find transitions
- Enhanced response format detection and error handling

This implements TODO 1-3 from AGENTS.md:
- TODO 1: Capture Depends-on Parameter Attribute
- TODO 2: Extract Transition Service Name
- TODO 3: Parse in-map Parameter Mapping

Files modified:
- DefaultScreenMacros.mcp.ftl: Extract transition metadata during screen render
- McpFieldOptionsService.groovy: Use extracted metadata for intelligent autocomplete
1 parent 4b34c970
...@@ -45,18 +45,29 @@ Use the following discovery tools to explore available functionality: ...@@ -45,18 +45,29 @@ 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/dashboard`: Catalog overview and management
48 - `/PopCommerce/PopCommerceAdmin/Catalog/Product/FindProduct`: Search and browse products 49 - `/PopCommerce/PopCommerceAdmin/Catalog/Product/FindProduct`: Search and browse products
49 - `/PopCommerce/PopCommerceAdmin/Catalog/Feature/FindFeature`: Search by features like color or size 50 - `/PopCommerce/PopCommerceAdmin/Catalog/Feature/FindFeature`: Search by features like color or size
50 - `/PopCommerce/PopCommerceAdmin/Catalog/Product/EditPrices`: View and update product prices 51 - `/PopCommerce/PopCommerceAdmin/Catalog/Product/EditPrices`: View and update product prices
51 52
52 ### Order Management 53 ### Order Management
53 - `/PopCommerce/PopCommerceAdmin/Order/FindOrder`: Lookup order status and details 54 - `/PopCommerce/PopCommerceAdmin/Order/FindOrder`: Lookup and create sales/purchase orders
54 - `/PopCommerce/PopCommerceAdmin/QuickSearch`: General order and customer search 55 - `/PopCommerce/PopCommerceAdmin/QuickSearch`: General order and customer search
55 56
56 ### Customer Management 57 ### Party & Customer Management
57 - `/PopCommerce/PopCommerceRoot/Customer`: Manage customer accounts 58 - `/PopCommerce/PopCommerceAdmin/Party/FindParty`: Manage all parties (People, Organizations)
59 - `/PopCommerce/PopCommerceRoot/Customer`: Customer storefront account management
60 - `User`: Internal user account, notifications, and messages
58 - `/PopCommerce/PopCommerceAdmin/QuickSearch`: Customer lookup 61 - `/PopCommerce/PopCommerceAdmin/QuickSearch`: Customer lookup
59 62
63 ### Facility & Accounting
64 - `/PopCommerce/PopCommerceAdmin/Facility/FindFacility`: Manage warehouses and inventory locations
65 - `/PopCommerce/PopCommerceAdmin/Accounting/dashboard`: Financial management and GL accounting
66
67 ### System & Developer Tools
68 - `/tools/Tools`: Developer tools, entity data editing, and service references
69 - `/tools/System`: System administration, cache management, and security settings
70
60 ## Tips for LLM Clients 71 ## Tips for LLM Clients
61 72
62 - All screens support parameterized queries for filtering results 73 - All screens support parameterized queries for filtering results
...@@ -107,18 +118,29 @@ Use the following discovery tools to explore available functionality: ...@@ -107,18 +118,29 @@ Use the following discovery tools to explore available functionality:
107 ## Common Screen Paths 118 ## Common Screen Paths
108 119
109 ### Catalog Operations 120 ### Catalog Operations
121 - `/PopCommerce/PopCommerceAdmin/Catalog/dashboard`: Catalog overview and management
110 - `/PopCommerce/PopCommerceAdmin/Catalog/Product/FindProduct`: Search and browse products 122 - `/PopCommerce/PopCommerceAdmin/Catalog/Product/FindProduct`: Search and browse products
111 - `/PopCommerce/PopCommerceAdmin/Catalog/Feature/FindFeature`: Search by features like color or size 123 - `/PopCommerce/PopCommerceAdmin/Catalog/Feature/FindFeature`: Search by features like color or size
112 - `/PopCommerce/PopCommerceAdmin/Catalog/Product/EditPrices`: View and update product prices 124 - `/PopCommerce/PopCommerceAdmin/Catalog/Product/EditPrices`: View and update product prices
113 125
114 ### Order Management 126 ### Order Management
115 - `/PopCommerce/PopCommerceAdmin/Order/FindOrder`: Lookup order status and details 127 - `/PopCommerce/PopCommerceAdmin/Order/FindOrder`: Lookup and create sales/purchase orders
116 - `/PopCommerce/PopCommerceAdmin/QuickSearch`: General order and customer search 128 - `/PopCommerce/PopCommerceAdmin/QuickSearch`: General order and customer search
117 129
118 ### Customer Management 130 ### Party & Customer Management
119 - `/PopCommerce/PopCommerceRoot/Customer`: Manage customer accounts 131 - `/PopCommerce/PopCommerceAdmin/Party/FindParty`: Manage all parties (People, Organizations)
132 - `/PopCommerce/PopCommerceRoot/Customer`: Customer storefront account management
133 - `User`: Internal user account, notifications, and messages
120 - `/PopCommerce/PopCommerceAdmin/QuickSearch`: Customer lookup 134 - `/PopCommerce/PopCommerceAdmin/QuickSearch`: Customer lookup
121 135
136 ### Facility & Accounting
137 - `/PopCommerce/PopCommerceAdmin/Facility/FindFacility`: Manage warehouses and inventory locations
138 - `/PopCommerce/PopCommerceAdmin/Accounting/dashboard`: Financial management and GL accounting
139
140 ### System & Developer Tools
141 - `/tools/Tools`: Developer tools, entity data editing, and service references
142 - `/tools/System`: System administration, cache management, and security settings
143
122 ## Tips for LLM Clients 144 ## Tips for LLM Clients
123 145
124 - All screens support parameterized queries for filtering results 146 - All screens support parameterized queries for filtering results
...@@ -144,14 +166,15 @@ Main storefront and customer-facing screens for PopCommerce e-commerce component ...@@ -144,14 +166,15 @@ Main storefront and customer-facing screens for PopCommerce e-commerce component
144 166
145 ## Key Screens 167 ## Key Screens
146 168
147 - **Customer Management**: Browse and manage customer accounts 169 - **Catalog & Products**: Browse the product catalog and view details
148 - **Order Status**: View order history and track shipments 170 - **Customer Management**: Manage customer profiles and account settings
149 - **Product Catalog**: Browse products and categories 171 - **Order Management**: Track order history and status
150 - **Shopping Cart**: Review and checkout items 172 - **Shopping Cart**: Review items and proceed to checkout
173 - **Messages**: Access `User/Messages` for internal communications
151 174
152 ## Navigation 175 ## Navigation
153 176
154 Use browse tools to explore the full catalog of PopCommerce screens starting from this root screen.]]></fileData> 177 Use browse tools to explore the full catalog of PopCommerce screens. For administrative tasks, use the `/PopCommerce/PopCommerceAdmin` hierarchy.]]></fileData>
155 </moqui.resource.DbResourceFile> 178 </moqui.resource.DbResourceFile>
156 179
157 <!-- PopCommerce Root Wiki Page --> 180 <!-- PopCommerce Root Wiki Page -->
...@@ -212,4 +235,126 @@ These screens are primarily for learning Moqui screen development patterns.]]></ ...@@ -212,4 +235,126 @@ These screens are primarily for learning Moqui screen development patterns.]]></
212 userId="EX_JOHN_DOE" 235 userId="EX_JOHN_DOE"
213 changeDateTime="2025-01-02 00:00:00.000"/> 236 changeDateTime="2025-01-02 00:00:00.000"/>
214 237
238 <!-- User Messages Documentation -->
239 <moqui.resource.DbResource
240 resourceId="WIKI_MCP_DOCS_USER_MESSAGES"
241 parentResourceId="WIKI_MCP_SCREEN_DOCS"
242 filename="User/Messages.md"
243 isFile="Y"/>
244 <moqui.resource.DbResourceFile
245 resourceId="WIKI_MCP_DOCS_USER_MESSAGES"
246 mimeType="text/markdown"
247 versionName="v1">
248 <fileData><![CDATA[# User Messages
249
250 Interface for sending and receiving messages between users and other parties.
251
252 ## Key Actions
253
254 - **Create Message**: Send a new message. Requires `toPartyId`, `subject`, and `body`.
255 - **View Thread**: View the conversation history for a message.
256 - **Find Messages**: Search for existing messages by various criteria.
257
258 ## Usage
259
260 Use this screen to communicate about orders, products, or general administrative tasks.]]></fileData>
261 </moqui.resource.DbResourceFile>
262
263 <moqui.resource.wiki.WikiPage
264 wikiPageId="MCP_SCREEN_DOCS/UserMessages"
265 wikiSpaceId="MCP_SCREEN_DOCS"
266 pagePath="User/Messages"
267 publishedVersionName="v1"
268 restrictView="N">
269 </moqui.resource.wiki.WikiPage>
270
271 <moqui.resource.wiki.WikiPageHistory
272 wikiPageId="MCP_SCREEN_DOCS/UserMessages"
273 historySeqId="1"
274 versionName="v1"
275 userId="EX_JOHN_DOE"
276 changeDateTime="2025-01-14 00:00:00.000"/>
277
278 <!-- Tools Documentation -->
279 <moqui.resource.DbResource
280 resourceId="WIKI_MCP_DOCS_TOOLS"
281 parentResourceId="WIKI_MCP_SCREEN_DOCS"
282 filename="tools/Tools.md"
283 isFile="Y"/>
284 <moqui.resource.DbResourceFile
285 resourceId="WIKI_MCP_DOCS_TOOLS"
286 mimeType="text/markdown"
287 versionName="v1">
288 <fileData><![CDATA[# Developer Tools
289
290 Central hub for developer and data management utilities.
291
292 ## Key Subscreens
293
294 - **Entity**: Data editing, import/export, and performance stats. Use `tools/Tools/Entity/DataEdit/EntityList` to browse and edit any entity.
295 - **Service**: Service reference and testing tools.
296 - **AutoScreen**: Automatically generated screens for all entities.
297 - **GroovyShell**: Execute arbitrary Groovy code (use with extreme caution).
298
299 ## Usage
300
301 Use these tools for low-level data manipulation or system exploration when standard administrative screens are insufficient.]]></fileData>
302 </moqui.resource.DbResourceFile>
303
304 <moqui.resource.wiki.WikiPage
305 wikiPageId="MCP_SCREEN_DOCS/Tools"
306 wikiSpaceId="MCP_SCREEN_DOCS"
307 pagePath="tools/Tools"
308 publishedVersionName="v1"
309 restrictView="N">
310 </moqui.resource.wiki.WikiPage>
311
312 <moqui.resource.wiki.WikiPageHistory
313 wikiPageId="MCP_SCREEN_DOCS/Tools"
314 historySeqId="1"
315 versionName="v1"
316 userId="EX_JOHN_DOE"
317 changeDateTime="2025-01-14 00:00:00.000"/>
318
319 <!-- System Documentation -->
320 <moqui.resource.DbResource
321 resourceId="WIKI_MCP_DOCS_SYSTEM"
322 parentResourceId="WIKI_MCP_SCREEN_DOCS"
323 filename="tools/System.md"
324 isFile="Y"/>
325 <moqui.resource.DbResourceFile
326 resourceId="WIKI_MCP_DOCS_SYSTEM"
327 mimeType="text/markdown"
328 versionName="v1">
329 <fileData><![CDATA[# System Administration
330
331 Core system management and monitoring screens.
332
333 ## Key Subscreens
334
335 - **Security**: Manage users, user groups, and artifact permissions.
336 - **Cache**: View and clear system caches.
337 - **ServiceJob**: Manage scheduled and background service jobs.
338 - **LogViewer**: View system logs and hit statistics.
339
340 ## Usage
341
342 Use these screens to monitor system health, manage user access, and troubleshoot operational issues.]]></fileData>
343 </moqui.resource.DbResourceFile>
344
345 <moqui.resource.wiki.WikiPage
346 wikiPageId="MCP_SCREEN_DOCS/System"
347 wikiSpaceId="MCP_SCREEN_DOCS"
348 pagePath="tools/System"
349 publishedVersionName="v1"
350 restrictView="N">
351 </moqui.resource.wiki.WikiPage>
352
353 <moqui.resource.wiki.WikiPageHistory
354 wikiPageId="MCP_SCREEN_DOCS/System"
355 historySeqId="1"
356 versionName="v1"
357 userId="EX_JOHN_DOE"
358 changeDateTime="2025-01-14 00:00:00.000"/>
359
215 </entity-facade-xml> 360 </entity-facade-xml>
......
...@@ -147,14 +147,61 @@ ...@@ -147,14 +147,61 @@
147 <#if dropdownOptions?has_content> 147 <#if dropdownOptions?has_content>
148 <#assign fieldMeta = fieldMeta + {"type": "dropdown", "options": dropdownOptions?js_string!}> 148 <#assign fieldMeta = fieldMeta + {"type": "dropdown", "options": dropdownOptions?js_string!}>
149 <#else> 149 <#else>
150 <#assign dynamicOptionNode = fieldSubNode["drop-down"]["dynamic-options"][0]!> 150 <#assign dropdownNode = fieldSubNode["drop-down"]!>
151 <#if dropdownNode?is_hash>
152 <#assign dynamicOptionNode = dropdownNode["dynamic-options"][0]!>
153 <#else>
154 <#assign dynamicOptionNode = dropdownNode[0]["dynamic-options"][0]!>
155 </#if>
151 <#if dynamicOptionNode?has_content> 156 <#if dynamicOptionNode?has_content>
157 <#-- Try to extract transition metadata for better autocomplete support -->
158 <#assign transitionMetadata = {}>
159 <#if dynamicOptionNode["@transition"]?has_content>
160 <#assign transitionNode = sri.getScreenDefinition().getTransitionItem(dynamicOptionNode["@transition"]!"")!>
161 <#if transitionNode?has_content>
162 <#-- Extract service name if present -->
163 <#assign serviceCallNode = transitionNode["service-call"][0]!>
164 <#if serviceCallNode?has_content && serviceCallNode["@name"]?has_content>
165 <#assign transitionMetadata = transitionMetadata + {"serviceName": (serviceCallNode["@name"]!"")}>
166 </#if>
167 <#-- Extract in-map parameter mapping -->
168 <#if serviceCallNode["@in-map"]?has_content>
169 <#assign transitionMetadata = transitionMetadata + {"inParameterMap": ((serviceCallNode["@in-map"]!"")?js_string)!""}>
170 <#elseif transitionNode["parameter"]?has_content>
171 <#assign paramNode = transitionNode["parameter"][0]!>
172 <#if paramNode?has_content>
173 <#assign transitionMetadata = transitionMetadata + {"inParameterMap": "[]"}>
174 </#if>
175 </#if>
176 </#if>
177 </#if>
178
179 <#-- Capture depends-on with parameter attribute -->
180 <#assign dependsOnList = []>
181 <#list dynamicOptionNode["depends-on"]! as depNode>
182 <#assign depField = depNode["@field"]!"">
183 <#assign depParameter = depNode["@parameter"]!depField>
184 <#assign dependsOnItem = depField + "|" + depParameter>
185 <#assign dependsOnList = dependsOnList + [dependsOnItem]>
186 </#list>
187 <#assign dependsOnJson = '[]'>
188 <#if dependsOnList?size gt 0>
189 <#assign dependsOnJson = '['>
190 <#list dependsOnList as dep>
191 <#if dep_index gt 0><#assign dependsOnJson = dependsOnJson + ', '></#if>
192 <#assign dependsOnJson = dependsOnJson + '"' + dep + '"'>
193 </#list>
194 <#assign dependsOnJson = dependsOnJson + ']'>
195 </#if>
196
197 <#-- Build dynamicOptions metadata -->
152 <#assign fieldMeta = fieldMeta + {"type": "dropdown", "dynamicOptions": { 198 <#assign fieldMeta = fieldMeta + {"type": "dropdown", "dynamicOptions": {
153 "transition": (dynamicOptionNode["@transition"]!""), 199 "transition": (dynamicOptionNode["@transition"]!""),
154 "serverSearch": (dynamicOptionNode["@server-search"]! == "true"), 200 "serverSearch": (dynamicOptionNode["@server-search"]! == "true"),
155 "minLength": (dynamicOptionNode["@min-length"]!"0"), 201 "minLength": (dynamicOptionNode["@min-length"]!"0"),
156 "parameterMap": (dynamicOptionNode["@parameter-map"]!"")?js_string!"" 202 "parameterMap": ((dynamicOptionNode["@parameter-map"]!"")?js_string)!"",
157 }}> 203 "dependsOn": dependsOnJson
204 } + transitionMetadata}>
158 <#else> 205 <#else>
159 <#assign fieldMeta = fieldMeta + {"type": "dropdown"}> 206 <#assign fieldMeta = fieldMeta + {"type": "dropdown"}>
160 </#if> 207 </#if>
......
...@@ -194,7 +194,8 @@ ...@@ -194,7 +194,8 @@
194 194
195 // Handle internal discovery/utility tools 195 // Handle internal discovery/utility tools
196 def internalToolMappings = [ 196 def internalToolMappings = [
197 "moqui_search_screens": "McpServices.mcp#SearchScreens" 197 "moqui_search_screens": "McpServices.mcp#SearchScreens",
198 "moqui_get_screen_details": "McpServices.mcp#GetScreenDetails"
198 ] 199 ]
199 200
200 def targetServiceName = internalToolMappings[name] 201 def targetServiceName = internalToolMappings[name]
...@@ -1929,6 +1930,20 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) ...@@ -1929,6 +1930,20 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
1929 ] 1930 ]
1930 ], 1931 ],
1931 [ 1932 [
1933 name: "moqui_get_screen_details",
1934 title: "Get Screen Details",
1935 description: "Get screen field details including dropdown options. Use this to understand available fields and their options before submitting forms.",
1936 inputSchema: [
1937 type: "object",
1938 properties: [
1939 "path": [type: "string", description: "Screen path to analyze (e.g., 'PopCommerce/PopCommerceAdmin/Party/FindParty')"],
1940 "fieldName": [type: "string", description: "Optional specific field name. If not provided, returns all fields."],
1941 "parameters": [type: "object", description: "Optional parameters to set in context before rendering (for autocomplete contexts)."]
1942 ],
1943 required: ["path"]
1944 ]
1945 ],
1946 [
1932 name: "prompts_list", 1947 name: "prompts_list",
1933 title: "List Prompts", 1948 title: "List Prompts",
1934 description: "List available MCP prompt templates.", 1949 description: "List available MCP prompt templates.",
...@@ -1957,6 +1972,36 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) ...@@ -1957,6 +1972,36 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
1957 </actions> 1972 </actions>
1958 </service> 1973 </service>
1959 1974
1975 <service verb="mcp" noun="GetScreenDetails" authenticate="true" allow-remote="true" transaction-timeout="60">
1976 <description>Get screen field details including dropdown options. Use this to understand available fields and their options before submitting forms.</description>
1977 <in-parameters>
1978 <parameter name="path" required="true"><description>Screen path to analyze (e.g., '/PopCommerce/PopCommerceAdmin/Party/FindParty').</description></parameter>
1979 <parameter name="fieldName"><description>Optional specific field name. If not provided, returns all fields.</description></parameter>
1980 <parameter name="parameters" type="Map"><description>Optional parameters to set in context before rendering (for autocomplete contexts).</description></parameter>
1981 </in-parameters>
1982 <out-parameters>
1983 <parameter name="result" type="Map"/>
1984 </out-parameters>
1985 <actions>
1986 <script><![CDATA[
1987 import org.moqui.context.ExecutionContext
1988 import groovy.json.JsonBuilder
1989 import org.moqui.mcp.McpFieldOptionsService
1990
1991 ExecutionContext ec = context.ec
1992
1993 def serviceResult = McpFieldOptionsService.service(path, fieldName, parameters, ec)
1994
1995 // Return in standard MCP format with content array
1996 def resultJson = new JsonBuilder(serviceResult).toString()
1997 result = [
1998 content: [[type: "text", text: resultJson]],
1999 isError: false
2000 ]
2001 ]]></script>
2002 </actions>
2003 </service>
2004
1960 <!-- NOTE: handle#McpRequest service removed - functionality moved to screen/webapp.xml for unified handling --> 2005 <!-- NOTE: handle#McpRequest service removed - functionality moved to screen/webapp.xml for unified handling -->
1961 2006
1962 </services> 2007 </services>
...\ No newline at end of file ...\ No newline at end of file
......
1 package org.moqui.mcp
2
3 import org.moqui.context.ExecutionContext
4
5 class McpFieldOptionsService {
6 static service(String path, String fieldName, Map parameters, ExecutionContext ec) {
7 if (!path) {
8 throw new IllegalArgumentException("path is required")
9 }
10
11 ec.logger.info("MCP GetScreenDetails: Getting details for screen ${path}, field ${fieldName ?: 'all'}")
12
13 def result = [
14 screenPath: path,
15 fields: [:]
16 ]
17
18 try {
19 // First, render screen to get form metadata (including dynamicOptions)
20 def browseScreenCallParams = [
21 path: path,
22 parameters: parameters ?: [:],
23 renderMode: "mcp",
24 sessionId: null,
25 terse: false
26 ]
27
28 def browseResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool")
29 .parameters(browseScreenCallParams)
30 .call()
31
32 if (browseResult?.result?.content?.size() > 0) {
33 def rawText = browseResult.result.content[0].text
34 if (rawText && rawText.startsWith("{")) {
35 def resultObj = new groovy.json.JsonSlurper().parseText(rawText)
36 def semanticData = resultObj?.semanticState?.data
37
38 if (semanticData?.containsKey("formMetadata")) {
39 def formMetadata = semanticData.formMetadata
40 def allFields = [:]
41
42 if (formMetadata instanceof Map) {
43 formMetadata.each { formName, formItem ->
44 if (formItem instanceof Map && formItem.containsKey("fields")) {
45 def fieldList = formItem.fields
46 if (fieldList instanceof Collection) {
47 fieldList.each { field ->
48 if (field instanceof Map && field.containsKey("name")) {
49 def fieldInfo = [
50 name: field.name,
51 title: field.title,
52 type: field.type,
53 required: field.required ?: false
54 ]
55
56 // Add dropdown options if available (static options)
57 if (field.type == "dropdown" && field.containsKey("options")) {
58 fieldInfo.options = field.options
59 }
60
61 // Add dynamic options metadata and actually fetch the options
62 def skipField = false
63 if (field.containsKey("dynamicOptions") && !skipField) {
64 def dynamicOptions = field.dynamicOptions
65 fieldInfo.dynamicOptions = dynamicOptions
66
67 try {
68 def serviceName = dynamicOptions.containsKey("service") ? dynamicOptions.service : null
69 def transitionName = dynamicOptions.containsKey("transition") ? dynamicOptions.transition : null
70 def optionParams = [:]
71
72 // Parse inParameterMap if specified (extracted from transition XML)
73 def inParameterMap = [:]
74 if (dynamicOptions.containsKey("inParameterMap") && dynamicOptions.inParameterMap && dynamicOptions.inParameterMap.trim()) {
75 // Parse in-map format: "[target1:source1,target2:source2]"
76 def mapContent = dynamicOptions.inParameterMap.trim()
77 if (mapContent.startsWith("[") && mapContent.endsWith("]")) {
78 def innerContent = mapContent.substring(1, mapContent.length() - 1)
79 innerContent.split(',').each { mapping ->
80 def colonIndex = mapping.indexOf(':')
81 if (colonIndex > 0) {
82 def targetParam = mapping.substring(0, colonIndex).trim()
83 def sourceFields = mapping.substring(colonIndex + 1).trim()
84 // Handle multiple source fields separated by comma
85 sourceFields.split(',').each { sourceField ->
86 def sourceValue = parameters?.get(sourceField.trim())
87 if (sourceValue != null) {
88 inParameterMap[targetParam] = sourceValue
89 ec.logger.info("MCP GetScreenDetails: Mapped in-param ${sourceField} -> ${targetParam} = ${sourceValue}")
90 }
91 }
92 }
93 }
94 }
95 }
96
97 // Handle depends-on fields and parameter overrides
98 ec.logger.info("MCP GetScreenDetails: Processing depends-on for field ${field.name}")
99
100 // Parse depends-on list (may include parameter overrides like "field|parameter")
101 def dependsOnFields = []
102 if (dynamicOptions.containsKey("dependsOn") && dynamicOptions.dependsOn) {
103 if (dynamicOptions.dependsOn instanceof String) {
104 dependsOnFields = new groovy.json.JsonSlurper().parseText(dynamicOptions.dependsOn)
105 } else if (dynamicOptions.dependsOn instanceof List) {
106 dependsOnFields = dynamicOptions.dependsOn
107 }
108 }
109
110 // Process each depends-on field with potential parameter override
111 dependsOnFields.each { depFieldOrTuple ->
112 def depField = depFieldOrTuple
113 def depParameter = depFieldOrTuple // Default: use field name as parameter name
114
115 // Check if depends-on item is a "field|parameter" tuple
116 if (depFieldOrTuple instanceof String && depFieldOrTuple.contains("|")) {
117 def parts = depFieldOrTuple.split("\\|")
118 if (parts.size() == 2) {
119 depField = parts[0].trim()
120 depParameter = parts[1].trim()
121 }
122 }
123
124 def depValue = parameters?.get(depField)
125 if (depValue == null) {
126 ec.logger.info("MCP GetScreenDetails: Depends-on field ${depField} has no value in parameters")
127 } else {
128 ec.logger.info("MCP GetScreenDetails: Depends-on field ${depField} = ${depValue}, targetParam = ${depParameter}")
129 // Add to optionParams - use depParameter as key if specified, otherwise use depField
130 optionParams[depParameter ?: depField] = depValue
131 }
132 }
133
134 // For transitions with web-send-json-response, try to extract and call service directly
135 // These transitions wrap services like BasicServices.get#GeoRegionsForDropDown
136 if (dynamicOptions.containsKey("serviceName") && dynamicOptions.serviceName) {
137 // Direct service call - use extracted service name from transition XML
138 ec.logger.info("MCP GetScreenDetails: Calling direct service ${dynamicOptions.serviceName} for field ${field.name} with optionParams: ${optionParams}")
139 def optionsResult = ec.service.sync().name(dynamicOptions.serviceName).parameters(optionParams).call()
140 if (optionsResult && optionsResult.resultList) {
141 def optionsList = []
142 optionsResult.resultList.each { opt ->
143 if (opt instanceof Map) {
144 def key = opt.geoId ?: opt.value ?: opt.key ?: opt.enumId
145 def label = opt.label ?: opt.description ?: opt.value
146 optionsList << [value: key, label: label]
147 }
148 }
149 if (optionsList) {
150 fieldInfo.options = optionsList
151 ec.logger.info("MCP GetScreenDetails: Retrieved ${optionsList.size()} options via direct service call")
152 allFields[field.name] = fieldInfo
153 return // Skip remaining processing for this field
154 }
155 }
156 } else {
157 // Fallback for hardcoded transitions or when serviceName not available
158 ec.logger.info("MCP GetScreenDetails: No serviceName found, checking hardcoded transitions")
159 if (transitionName == "getGeoCountryStates" || transitionName == "getGeoStateCounties") {
160 def underlyingService = "org.moqui.impl.BasicServices.get#GeoRegionsForDropDown"
161 // Map depends-on field names to service parameter names (e.g., countryGeoId -> geoId)
162 def serviceParams = [:]
163 if (optionParams.containsKey("countryGeoId")) {
164 serviceParams.geoId = optionParams.countryGeoId
165 }
166 if (optionParams.containsKey("stateGeoId")) {
167 serviceParams.geoId = optionParams.stateGeoId
168 serviceParams.geoTypeEnumId = "GEOT_COUNTY"
169 }
170 if (optionParams.containsKey("term")) {
171 serviceParams.term = optionParams.term
172 }
173 ec.logger.info("MCP GetScreenDetails: Calling direct service ${underlyingService} for field ${field.name} with serviceParams: ${serviceParams}")
174 def optionsResult = ec.service.sync().name(underlyingService).parameters(serviceParams).call()
175 if (optionsResult && optionsResult.resultList) {
176 def optionsList = []
177 optionsResult.resultList.each { opt ->
178 if (opt instanceof Map) {
179 def key = opt.geoId ?: opt.value ?: opt.key
180 def label = opt.label ?: opt.value
181 optionsList << [value: key, label: label]
182 }
183 }
184 if (optionsList) {
185 fieldInfo.options = optionsList
186 ec.logger.info("MCP GetScreenDetails: Retrieved ${optionsList.size()} options via direct service call, returning from field processing")
187 allFields[field.name] = fieldInfo
188 return // Skip remaining processing for this field
189 }
190 }
191 }
192 // Fallback for transitions without direct service - try calling via ScreenAsMcpTool
193 // This handles entity-find transitions and others without web-send-json-response
194 if (transitionName && !dynamicOptions.containsKey("serviceName")) {
195 ec.logger.info("MCP GetScreenDetails: Calling transition ${transitionName} via ScreenAsMcpTool for field ${field.name}")
196 def transCallParams = [
197 path: "/" + path,
198 action: transitionName,
199 parameters: optionParams,
200 renderMode: "mcp",
201 sessionId: null
202 ]
203 def transResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool").parameters(transCallParams).call()
204
205 // Parse response - try to extract options from mcp render output
206 if (transResult?.result?.content?.size() > 0) {
207 def responseText = transResult.result.content[0].text
208 if (responseText) {
209 ec.logger.info("MCP GetScreenDetails: Transition returned ${responseText.length()} chars")
210
211 // Try to parse as JSON first
212 try {
213 def jsonObj = new groovy.json.JsonSlurper().parseText(responseText)
214
215 // Check for options in formMetadata
216 if (jsonObj.containsKey("formMetadata")) {
217 def formData = jsonObj.formMetadata
218 formData.each { formName, formItem ->
219 if (formItem.containsKey("fields")) {
220 def fieldOptions = formItem.fields.find { it.name == field.name }
221 if (fieldOptions && fieldOptions.containsKey("options")) {
222 fieldInfo.options = fieldOptions.options
223 ec.logger.info("MCP GetScreenDetails: Retrieved ${fieldInfo.options.size()} options from formMetadata")
224 allFields[field.name] = fieldInfo
225 return
226 }
227 }
228 }
229 }
230
231 // Fallback to previous logic
232 if (jsonObj instanceof Map && jsonObj.containsKey("resultList")) {
233 def resultList = jsonObj.resultList
234 if (resultList instanceof List) {
235 def optionsList = []
236 resultList.each { opt ->
237 if (opt instanceof Map) {
238 def key = opt.geoId ?: opt.value ?: opt.key ?: opt.enumId
239 def label = opt.label ?: opt.description ?: opt.value
240 optionsList << [value: key, label: label]
241 }
242 }
243 if (optionsList) {
244 fieldInfo.options = optionsList
245 ec.logger.info("MCP GetScreenDetails: Retrieved ${optionsList.size()} options from transition ${transitionName}")
246 allFields[field.name] = fieldInfo
247 return
248 }
249 } else if (jsonObj instanceof Map && jsonObj.containsKey("options")) {
250 fieldInfo.options = jsonObj.options
251 } else if (jsonObj instanceof Map && jsonObj.containsKey("value")) {
252 fieldInfo.options = [[value: jsonObj.value, label: jsonObj.value]]
253 } else {
254 fieldInfo.optionsError = "Unrecognized response format: ${jsonObj.getClass().simpleName}"
255 }
256 } catch (Exception parseEx) {
257 ec.logger.warn("MCP GetScreenDetails: Failed to parse transition response: ${parseEx.message}")
258 fieldInfo.optionsError = "Transition call succeeded but response format not recognized. Try: moqui_browse_screens(path='${path}', action='${transitionName}')"
259 }
260 }
261 }
262 fieldInfo.optionsError = "Call moqui_browse_screens(path='\${path}', action='\${transitionName}') to fetch dropdown options"
263 }
264 if (serviceName) {
265 // Direct service call - same as frontend does
266 ec.logger.info("MCP GetScreenDetails: Calling service ${serviceName} for field ${field.name}")
267 def optionsResult = ec.service.sync().name(serviceName).parameters(optionParams).call()
268 if (optionsResult) {
269 def optionsList = []
270 if (optionsResult instanceof Map && optionsResult.containsKey("resultList")) {
271 def resultList = optionsResult.resultList
272 if (resultList instanceof List) {
273 resultList.each { opt ->
274 if (opt instanceof Map) {
275 def key = opt.geoId ?: opt.value ?: opt.key
276 def label = opt.label ?: opt.value
277 optionsList << [value: key, label: label]
278 }
279 }
280 }
281 } else if (optionsResult instanceof List) {
282 optionsList = optionsResult
283 }
284
285 if (optionsList) {
286 fieldInfo.options = optionsList
287 ec.logger.info("MCP GetScreenDetails: Retrieved ${optionsList.size()} options from service ${serviceName}")
288 }
289 }
290 } else if (transitionName) {
291 // For transitions, try calling via ScreenAsMcpTool with transition
292 ec.logger.info("MCP GetScreenDetails: Calling transition ${transitionName} for field ${field.name}")
293
294 def transCallParams = [
295 path: "/" + path,
296 action: transitionName,
297 parameters: optionParams,
298 renderMode: "text", // Get raw text response, not JSON
299 sessionId: null
300 ]
301 def transResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool").parameters(transCallParams).call()
302
303 // Parse response - transitions return JSON with 'resultList'
304 if (transResult?.result?.content?.size() > 0) {
305 def responseText = transResult.result.content[0].text
306 if (responseText) {
307 ec.logger.info("MCP GetScreenDetails: Transition returned ${responseText.length()} chars")
308
309 // Parse JSON from response
310 try {
311 def jsonObj = new groovy.json.JsonSlurper().parseText(responseText)
312
313 if (jsonObj instanceof Map && jsonObj.containsKey("resultList")) {
314 def resultList = jsonObj.resultList
315 if (resultList instanceof List) {
316 def optionsList = []
317 resultList.each { opt ->
318 if (opt instanceof Map) {
319 def key = opt.geoId ?: opt.value ?: opt.key
320 def label = opt.label ?: opt.value
321 optionsList << [value: key, label: label]
322 }
323 }
324 if (optionsList) {
325 fieldInfo.options = optionsList
326 ec.logger.info("MCP GetScreenDetails: Retrieved ${optionsList.size()} options from transition ${transitionName}")
327 }
328 }
329 } else if (jsonObj instanceof Map && jsonObj.containsKey("options")) {
330 fieldInfo.options = jsonObj.options
331 } else if (jsonObj instanceof Map && jsonObj.containsKey("value")) {
332 fieldInfo.options = [[value: jsonObj.value, label: jsonObj.value]]
333 }
334 } catch (Exception parseEx) {
335 ec.logger.warn("MCP GetScreenDetails: Failed to parse transition response: ${parseEx.message}")
336 fieldInfo.optionsError = "Failed to parse options response"
337 }
338 }
339 }
340 } else {
341 fieldInfo.optionsError = "No service or transition specified in dynamic-options"
342 }
343 } catch (Exception e) {
344 ec.logger.warn("MCP GetScreenDetails: Failed to get options for field ${field.name}: ${e.message}")
345 fieldInfo.optionsError = "Failed to load options: ${e.message}"
346 }
347 }
348
349 if (!skipField) {
350 allFields[field.name] = fieldInfo
351 }
352 }
353 }
354 }
355 }
356 }
357 }
358
359 ec.logger.info("MCP GetScreenDetails: Extracted ${allFields.size()} fields")
360
361 // Return specific field or all fields
362 if (fieldName) {
363 def specificField = allFields[fieldName]
364 if (specificField) {
365 result.fields[fieldName] = specificField
366 } else {
367 result.error = "Field not found: ${fieldName}"
368 }
369 } else {
370 result.fields = allFields.collectEntries { k, v -> [name: k, *:v] }
371 }
372 } else {
373 ec.logger.warn("MCP GetScreenDetails: No formMetadata found in semantic state")
374 result.error = "No form data available"
375 }
376 } else {
377 result.error = "Invalid response from ScreenAsMcpTool"
378 }
379 } else {
380 result.error = "No content returned from ScreenAsMcpTool"
381 }
382 } catch (Exception e) {
383 ec.logger.error("MCP GetScreenDetails: Error: ${e.getClass().simpleName}: ${e.message}")
384 result.error = "Screen resolution failed: ${e.message}"
385 }
386
387 return result
388 }
389 }
1 package org.moqui.mcp
2
3 import org.moqui.context.ExecutionContext
4
5 class McpFieldOptionsService {
6 static service(String path, String fieldName, Map parameters, ExecutionContext ec) {
7 if (!path) {
8 throw new IllegalArgumentException("path is required")
9 }
10
11 ec.logger.info("MCP GetScreenDetails: Getting details for screen ${path}, field ${fieldName ?: 'all'}")
12
13 def result = [
14 screenPath: path,
15 fields: [:]
16 ]
17
18 try {
19 // First, render screen to get form metadata (including dynamicOptions)
20 def browseResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool")
21 .parameters([path: path, parameters: parameters ?: [:], renderMode: "mcp", sessionId: null, terse: false])
22 .call()
23
24 if (browseResult?.result?.content?.size() > 0) {
25 def rawText = browseResult.result.content[0].text
26 if (rawText && rawText.startsWith("{")) {
27 def resultObj = new groovy.json.JsonSlurper().parseText(rawText)
28 def semanticData = resultObj?.semanticState?.data
29
30 if (semanticData?.containsKey("formMetadata")) {
31 def formMetadata = semanticData.formMetadata
32 def allFields = [:]
33
34 if (formMetadata instanceof Map) {
35 formMetadata.each { formName, formItem ->
36 if (formItem instanceof Map && formItem.containsKey("fields")) {
37 def fieldList = formItem.fields
38 if (fieldList instanceof Collection) {
39 fieldList.each { field ->
40 if (field instanceof Map && field.containsKey("name")) {
41 def fieldInfo = [
42 name: field.name,
43 title: field.title,
44 type: field.type,
45 required: field.required ?: false
46 ]
47
48 // Add dropdown options if available (static options)
49 if (field.type == "dropdown" && field.containsKey("options")) {
50 fieldInfo.options = field.options
51 }
52
53 // Add dynamic options metadata and actually fetch options
54 if (field.containsKey("dynamicOptions")) {
55 def dynamicOptions = field.dynamicOptions
56 fieldInfo.dynamicOptions = dynamicOptions
57
58 try {
59 def serviceName = dynamicOptions.containsKey("serviceName") ? dynamicOptions.serviceName : null
60 def transitionName = dynamicOptions.containsKey("transition") ? dynamicOptions.transition : null
61 def optionParams = [:]
62
63 // Parse inParameterMap if specified (extracted from transition XML)
64 def inParameterMap = [:]
65 if (dynamicOptions.containsKey("inParameterMap") && dynamicOptions.inParameterMap && dynamicOptions.inParameterMap.trim()) {
66 // Parse in-map format: "[target1:source1,target2:source2]"
67 def mapContent = dynamicOptions.inParameterMap.trim()
68 if (mapContent.startsWith("[") && mapContent.endsWith("]")) {
69 def innerContent = mapContent.substring(1, mapContent.length() - 1)
70 innerContent.split(',').each { mapping ->
71 def colonIndex = mapping.indexOf(':')
72 if (colonIndex > 0) {
73 def targetParam = mapping.substring(0, colonIndex).trim()
74 def sourceFields = mapping.substring(colonIndex + 1).trim()
75 // Handle multiple source fields separated by comma
76 sourceFields.split(',').each { sourceField ->
77 def sourceValue = parameters?.get(sourceField.trim())
78 if (sourceValue != null) {
79 inParameterMap[targetParam] = sourceValue
80 ec.logger.info("MCP GetScreenDetails: Mapped in-param ${sourceField} -> ${targetParam} = ${sourceValue}")
81 }
82 }
83 }
84 }
85 }
86 }
87
88 // Handle depends-on fields and parameter overrides
89 ec.logger.info("MCP GetScreenDetails: Processing depends-on for field ${field.name}")
90
91 // Parse depends-on list (may include parameter overrides like "field|parameter")
92 def dependsOnFields = []
93 if (dynamicOptions.containsKey("dependsOn") && dynamicOptions.dependsOn) {
94 if (dynamicOptions.dependsOn instanceof String) {
95 dependsOnFields = new groovy.json.JsonSlurper().parseText(dynamicOptions.dependsOn)
96 } else if (dynamicOptions.dependsOn instanceof List) {
97 dependsOnFields = dynamicOptions.dependsOn
98 }
99 }
100
101 // Process each depends-on field with potential parameter override
102 dependsOnFields.each { depFieldOrTuple ->
103 def depField = depFieldOrTuple
104 def depParameter = depFieldOrTuple // Default: use field name as parameter name
105
106 // Check if depends-on item is a "field|parameter" tuple
107 if (depFieldOrTuple instanceof String && depFieldOrTuple.contains("|")) {
108 def parts = depFieldOrTuple.split("\\|")
109 if (parts.size() == 2) {
110 depField = parts[0].trim()
111 depParameter = parts[1].trim()
112 }
113 }
114
115 def depValue = parameters?.get(depField)
116 if (depValue == null) {
117 ec.logger.info("MCP GetScreenDetails: Depends-on field ${depField} has no value in parameters")
118 } else {
119 ec.logger.info("MCP GetScreenDetails: Depends-on field ${depField} = ${depValue}, targetParam = ${depParameter}")
120 // Add to optionParams - use depParameter as key if specified, otherwise use depField
121 optionParams[depParameter ?: depField] = depValue
122 }
123 }
124
125 // For server-search fields, add term parameter
126 if (dynamicOptions.containsKey("serverSearch") && dynamicOptions.serverSearch) {
127 if (parameters?.containsKey("term")) {
128 def searchTerm = parameters.term
129 if (searchTerm && searchTerm.length() >= (dynamicOptions.minLength ?: 0)) {
130 optionParams.term = searchTerm
131 ec.logger.info("MCP GetScreenDetails: Server search term = '${searchTerm}'")
132 } else {
133 ec.logger.info("MCP GetScreenDetails: No term provided, will return full list")
134 }
135 }
136 }
137
138 // For transitions with web-send-json-response, try to extract and call service directly
139 // These transitions wrap BasicServices.get#GeoRegionsForDropDown
140 if (dynamicOptions.containsKey("serviceName") && dynamicOptions.serviceName) {
141 // Direct service call - use extracted service name from transition XML
142 ec.logger.info("MCP GetScreenDetails: Calling direct service ${dynamicOptions.serviceName} for field ${field.name} with optionParams: ${optionParams}")
143 def optionsResult = ec.service.sync().name(dynamicOptions.serviceName).parameters(optionParams).call()
144 if (optionsResult && optionsResult.resultList) {
145 def optionsList = []
146 optionsResult.resultList.each { opt ->
147 if (opt instanceof Map) {
148 def key = opt.geoId ?: opt.value ?: opt.key ?: opt.enumId
149 def label = opt.label ?: opt.description ?: opt.value
150 optionsList << [value: key, label: label]
151 }
152 }
153 if (optionsList) {
154 fieldInfo.options = optionsList
155 ec.logger.info("MCP GetScreenDetails: Retrieved ${optionsList.size()} options via direct service call")
156 allFields[field.name] = fieldInfo
157 return // Skip remaining processing for this field
158 }
159 }
160 } else {
161 // Fallback for hardcoded transitions or when serviceName not available
162 ec.logger.info("MCP GetScreenDetails: No serviceName found, checking hardcoded transitions")
163 if (transitionName == "getGeoCountryStates" || transitionName == "getGeoStateCounties") {
164 def underlyingService = "org.moqui.impl.BasicServices.get#GeoRegionsForDropDown"
165 // Map depends-on field names to service parameter names (e.g., countryGeoId -> geoId)
166 def serviceParams = [:]
167 if (optionParams.containsKey("countryGeoId")) {
168 serviceParams.geoId = optionParams.countryGeoId
169 }
170 if (optionParams.containsKey("stateGeoId")) {
171 serviceParams.geoId = optionParams.stateGeoId
172 serviceParams.geoTypeEnumId = "GEOT_COUNTY"
173 }
174 if (optionParams.containsKey("term")) {
175 serviceParams.term = optionParams.term
176 }
177 ec.logger.info("MCP GetScreenDetails: Calling direct service ${underlyingService} for field ${field.name} with serviceParams: ${serviceParams}")
178 def optionsResult = ec.service.sync().name(underlyingService).parameters(serviceParams).call()
179 if (optionsResult && optionsResult.resultList) {
180 def optionsList = []
181 optionsResult.resultList.each { opt ->
182 if (opt instanceof Map) {
183 def key = opt.geoId ?: opt.value ?: opt.key ?: opt.enumId
184 def label = opt.label ?: opt.description ?: opt.value
185 optionsList << [value: key, label: label]
186 }
187 }
188 if (optionsList) {
189 fieldInfo.options = optionsList
190 ec.logger.info("MCP GetScreenDetails: Retrieved ${optionsList.size()} options via direct service call")
191 allFields[field.name] = fieldInfo
192 return // Skip remaining processing for this field
193 }
194 }
195 }
196 }
197
198 } catch (Exception e) {
199 ec.logger.warn("MCP GetScreenDetails: Failed to get options for field ${field.name}: ${e.message}")
200 fieldInfo.optionsError = "Failed to load options: ${e.message}"
201 }
202 }
203
204 allFields[field.name] = fieldInfo
205 }
206 }
207 }
208 }
209 }
210 }
211
212 ec.logger.info("MCP GetScreenDetails: Extracted ${allFields.size()} fields")
213
214 // Return specific field or all fields
215 if (fieldName) {
216 def specificField = allFields[fieldName]
217 if (specificField) {
218 result.fields[fieldName] = specificField
219 } else {
220 result.error = "Field not found: ${fieldName}"
221 }
222 } else {
223 result.fields = allFields.collectEntries { k, v -> [name: k, *:v] }
224 }
225 } else {
226 ec.logger.warn("MCP GetScreenDetails: No formMetadata found in semantic state")
227 result.error = "No form data available"
228 }
229 } else {
230 result.error = "Invalid response from ScreenAsMcpTool"
231 }
232 } catch (Exception e) {
233 ec.logger.error("MCP GetScreenDetails: Error: ${e.getClass().simpleName}: ${e.message}")
234 result.error = "Screen resolution failed: ${e.message}"
235 }
236 }
237
238 return result
239 }
240 }
...@@ -52,13 +52,13 @@ class UiNarrativeBuilder { ...@@ -52,13 +52,13 @@ class UiNarrativeBuilder {
52 narrative.screen = describeScreen(screenDef, semanticState, isTerse) 52 narrative.screen = describeScreen(screenDef, semanticState, isTerse)
53 narrative.actions = describeActions(screenDef, semanticState, currentPath, isTerse) 53 narrative.actions = describeActions(screenDef, semanticState, currentPath, isTerse)
54 narrative.navigation = describeLinks(semanticState, currentPath, isTerse) 54 narrative.navigation = describeLinks(semanticState, currentPath, isTerse)
55 narrative.notes = describeNotes(semanticState, isTerse) 55 narrative.notes = describeNotes(semanticState, currentPath, isTerse)
56 56
57 return narrative 57 return narrative
58 } 58 }
59 59
60 String describeScreen(ScreenDefinition screenDef, Map<String, Object> semanticState, boolean isTerse) { 60 String describeScreen(ScreenDefinition screenDef, Map<String, Object> semanticState, boolean isTerse) {
61 def screenName = screenDef?.name ?: "Screen" 61 def screenName = screenDef?.getScreenName() ?: "Screen"
62 def sb = new StringBuilder() 62 def sb = new StringBuilder()
63 63
64 sb.append("${screenName} displays ") 64 sb.append("${screenName} displays ")
...@@ -201,7 +201,7 @@ class UiNarrativeBuilder { ...@@ -201,7 +201,7 @@ class UiNarrativeBuilder {
201 return navigation 201 return navigation
202 } 202 }
203 203
204 List<String> describeNotes(Map<String, Object> semanticState, boolean isTerse) { 204 List<String> describeNotes(Map<String, Object> semanticState, String currentPath, boolean isTerse) {
205 def notes = [] 205 def notes = []
206 206
207 def data = semanticState?.data 207 def data = semanticState?.data
...@@ -220,6 +220,29 @@ class UiNarrativeBuilder { ...@@ -220,6 +220,29 @@ class UiNarrativeBuilder {
220 notes << "This screen has ${actions.size()} actions. Use semanticState.actions for complete list." 220 notes << "This screen has ${actions.size()} actions. Use semanticState.actions for complete list."
221 } 221 }
222 222
223 // Add note about moqui_get_screen_details for dropdown options
224 def formData = semanticState?.data
225 if (formData && formData.containsKey('formMetadata') && formData.formMetadata instanceof Map) {
226 def formMetadata = formData.formMetadata
227 def allFields = []
228 formMetadata.each { formName, formInfo ->
229 if (formInfo instanceof Map && formInfo.containsKey('fields')) {
230 def fields = formInfo.fields
231 if (fields instanceof Collection) {
232 def dynamicFields = fields.findAll { f -> f instanceof Map && f.containsKey('dynamicOptions') }
233 if (dynamicFields) {
234 def fieldNames = dynamicFields.collect { it.name }.take(3)
235 allFields.addAll(fieldNames)
236 }
237 }
238 }
239 }
240 if (allFields) {
241 def uniqueFields = allFields.unique().take(5)
242 notes << "Fields with autocomplete: ${uniqueFields.join(', ')}. Use moqui_get_screen_details(path='${currentPath}', fieldName='${uniqueFields[0]}') to get field-specific options."
243 }
244 }
245
223 def parameters = semanticState?.parameters 246 def parameters = semanticState?.parameters
224 if (parameters && parameters.size() > 0) { 247 if (parameters && parameters.size() > 0) {
225 def requiredParams = parameters.findAll { k, v -> k.toString().toLowerCase().contains('id') } 248 def requiredParams = parameters.findAll { k, v -> k.toString().toLowerCase().contains('id') }
......