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
Showing
6 changed files
with
907 additions
and
18 deletions
| ... | @@ -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') } | ... | ... |
-
Please register or sign in to post a comment