Improve MCP usability: truncate dropdowns, add summary field
- Remove remaining isTerse references from UiNarrativeBuilder - Truncate dropdown options to 10 in browse_screens with fetchHint - get_screen_details returns full options via mcpFullOptions flag - Extract wiki summary as top-level field in responses - Add extractSummary helper for first non-header paragraph
Showing
4 changed files
with
62 additions
and
11 deletions
| ... | @@ -159,12 +159,24 @@ | ... | @@ -159,12 +159,24 @@ |
| 159 | <#assign dropdownOptions = sri.getFieldOptions(dropdownNode)!> | 159 | <#assign dropdownOptions = sri.getFieldOptions(dropdownNode)!> |
| 160 | <#if (dropdownOptions?size!0) gt 0> | 160 | <#if (dropdownOptions?size!0) gt 0> |
| 161 | <#-- Build options list from the LinkedHashMap --> | 161 | <#-- Build options list from the LinkedHashMap --> |
| 162 | <#-- Truncate if > 10 unless mcpFullOptions is set (for get_screen_details) --> | ||
| 162 | <#assign optionsList = []> | 163 | <#assign optionsList = []> |
| 164 | <#assign totalOptions = dropdownOptions?size> | ||
| 165 | <#assign skipTruncation = (ec.context.mcpFullOptions!false) == true> | ||
| 166 | <#assign optionLimit = skipTruncation?then(999999, 10)> | ||
| 167 | <#assign optionCount = 0> | ||
| 163 | <#list (dropdownOptions.keySet())! as optKey> | 168 | <#list (dropdownOptions.keySet())! as optKey> |
| 164 | <#assign optLabel = (dropdownOptions.get(optKey))!optKey> | 169 | <#if optionCount lt optionLimit> |
| 165 | <#assign optionsList = optionsList + [{"value": optKey, "label": optLabel}]> | 170 | <#assign optLabel = (dropdownOptions.get(optKey))!optKey> |
| 171 | <#assign optionsList = optionsList + [{"value": optKey, "label": optLabel}]> | ||
| 172 | </#if> | ||
| 173 | <#assign optionCount = optionCount + 1> | ||
| 166 | </#list> | 174 | </#list> |
| 167 | <#assign fieldMeta = fieldMeta + {"type": "dropdown", "options": optionsList}> | 175 | <#if (totalOptions gt 10) && !skipTruncation> |
| 176 | <#assign fieldMeta = fieldMeta + {"type": "dropdown", "options": optionsList, "optionsTruncated": true, "totalOptions": totalOptions, "fetchHint": "Use moqui_get_screen_details(fieldName='" + (fieldNode["@name"]!"") + "') for all " + totalOptions + " options"}> | ||
| 177 | <#else> | ||
| 178 | <#assign fieldMeta = fieldMeta + {"type": "dropdown", "options": optionsList}> | ||
| 179 | </#if> | ||
| 168 | <#else> | 180 | <#else> |
| 169 | <#-- No static options - check for dynamic-options --> | 181 | <#-- No static options - check for dynamic-options --> |
| 170 | <#assign dynamicOptionsList = dropdownNode["dynamic-options"]!> | 182 | <#assign dynamicOptionsList = dropdownNode["dynamic-options"]!> |
| ... | @@ -281,12 +293,24 @@ | ... | @@ -281,12 +293,24 @@ |
| 281 | </#list> | 293 | </#list> |
| 282 | <#assign dropdownOptions = sri.getFieldOptions(fieldSubNode)!> | 294 | <#assign dropdownOptions = sri.getFieldOptions(fieldSubNode)!> |
| 283 | <#if dropdownOptions?has_content && dropdownOptions?size gt 0> | 295 | <#if dropdownOptions?has_content && dropdownOptions?size gt 0> |
| 284 | <#-- Convert LinkedHashMap<String,String> to list of {value, label} objects for JSON --> | 296 | <#-- Convert LinkedHashMap<String,String> to list of {value, label} objects --> |
| 297 | <#-- Truncate if > 10 unless mcpFullOptions is set (for get_screen_details) --> | ||
| 285 | <#assign optionsList = []> | 298 | <#assign optionsList = []> |
| 299 | <#assign totalOptions = dropdownOptions?size> | ||
| 300 | <#assign skipTruncation = (ec.context.mcpFullOptions!false) == true> | ||
| 301 | <#assign optionLimit = skipTruncation?then(999999, 10)> | ||
| 302 | <#assign optionCount = 0> | ||
| 286 | <#list dropdownOptions?keys as optKey> | 303 | <#list dropdownOptions?keys as optKey> |
| 287 | <#assign optionsList = optionsList + [{"value": optKey, "label": dropdownOptions[optKey]!optKey}]> | 304 | <#if optionCount lt optionLimit> |
| 305 | <#assign optionsList = optionsList + [{"value": optKey, "label": dropdownOptions[optKey]!optKey}]> | ||
| 306 | </#if> | ||
| 307 | <#assign optionCount = optionCount + 1> | ||
| 288 | </#list> | 308 | </#list> |
| 289 | <#assign fieldMeta = fieldMeta + {"type": "dropdown", "options": optionsList}> | 309 | <#if (totalOptions gt 10) && !skipTruncation> |
| 310 | <#assign fieldMeta = fieldMeta + {"type": "dropdown", "options": optionsList, "optionsTruncated": true, "totalOptions": totalOptions, "fetchHint": "Use moqui_get_screen_details(fieldName='" + (fieldNode["@name"]!"") + "') for all " + totalOptions + " options"}> | ||
| 311 | <#else> | ||
| 312 | <#assign fieldMeta = fieldMeta + {"type": "dropdown", "options": optionsList}> | ||
| 313 | </#if> | ||
| 290 | <#else> | 314 | <#else> |
| 291 | <#assign dropdownNode = fieldSubNode["drop-down"]!> | 315 | <#assign dropdownNode = fieldSubNode["drop-down"]!> |
| 292 | 316 | ... | ... |
| ... | @@ -601,6 +601,21 @@ def getSimplePath = { fullPath -> | ... | @@ -601,6 +601,21 @@ def getSimplePath = { fullPath -> |
| 601 | return parts.join('/') | 601 | return parts.join('/') |
| 602 | } | 602 | } |
| 603 | 603 | ||
| 604 | // Helper to extract short description/summary from wiki content | ||
| 605 | def extractSummary = { wikiText -> | ||
| 606 | if (!wikiText) return null | ||
| 607 | def textString = wikiText instanceof String ? wikiText : new String(wikiText, "UTF-8") | ||
| 608 | def lines = textString.split('\n') | ||
| 609 | for (def line : lines) { | ||
| 610 | def trimmed = line.trim() | ||
| 611 | // Skip empty lines and headers | ||
| 612 | if (trimmed && !trimmed.startsWith('#')) { | ||
| 613 | return trimmed.take(200) | ||
| 614 | } | ||
| 615 | } | ||
| 616 | return null | ||
| 617 | } | ||
| 618 | |||
| 604 | // Helper function to load wiki instructions for a screen | 619 | // Helper function to load wiki instructions for a screen |
| 605 | def getWikiInstructions = { lookupPath -> | 620 | def getWikiInstructions = { lookupPath -> |
| 606 | try { | 621 | try { |
| ... | @@ -1061,7 +1076,11 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) | ... | @@ -1061,7 +1076,11 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) |
| 1061 | if (output) { | 1076 | if (output) { |
| 1062 | mcpResult.textPreview = output.take(2000) + (output.length() > 2000 ? "..." : "") | 1077 | mcpResult.textPreview = output.take(2000) + (output.length() > 2000 ? "..." : "") |
| 1063 | } | 1078 | } |
| 1064 | if (wikiInstructions) mcpResult.wikiInstructions = wikiInstructions | 1079 | if (wikiInstructions) { |
| 1080 | mcpResult.wikiInstructions = wikiInstructions | ||
| 1081 | def summary = extractSummary(wikiInstructions) | ||
| 1082 | if (summary) mcpResult.summary = summary | ||
| 1083 | } | ||
| 1065 | 1084 | ||
| 1066 | content << [ | 1085 | content << [ |
| 1067 | type: "text", | 1086 | type: "text", |
| ... | @@ -1781,6 +1800,11 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) | ... | @@ -1781,6 +1800,11 @@ def wikiInstructions = getWikiInstructions(inputScreenPath) |
| 1781 | 1800 | ||
| 1782 | if (wikiInstructions) { | 1801 | if (wikiInstructions) { |
| 1783 | resultMap.wikiInstructions = wikiInstructions | 1802 | resultMap.wikiInstructions = wikiInstructions |
| 1803 | // Extract first non-header paragraph as summary | ||
| 1804 | def summary = getShortDescription(wikiInstructions) | ||
| 1805 | if (summary) { | ||
| 1806 | resultMap.summary = summary | ||
| 1807 | } | ||
| 1784 | } | 1808 | } |
| 1785 | 1809 | ||
| 1786 | if (renderError) { | 1810 | if (renderError) { | ... | ... |
| ... | @@ -23,8 +23,11 @@ class McpFieldOptionsService { | ... | @@ -23,8 +23,11 @@ class McpFieldOptionsService { |
| 23 | 23 | ||
| 24 | def result = [screenPath: path, fields: [:]] | 24 | def result = [screenPath: path, fields: [:]] |
| 25 | try { | 25 | try { |
| 26 | // Pass mcpFullOptions through parameters to get full dropdown options without truncation | ||
| 27 | def mergedParams = (parameters ?: [:]) + [mcpFullOptions: true] | ||
| 28 | |||
| 26 | def browseResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool") | 29 | def browseResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool") |
| 27 | .parameters([path: path, parameters: parameters ?: [:], renderMode: "mcp", sessionId: null]) | 30 | .parameters([path: path, parameters: mergedParams, renderMode: "mcp", sessionId: null]) |
| 28 | .call() | 31 | .call() |
| 29 | 32 | ||
| 30 | ec.logger.info("=== browseResult: ${browseResult != null}, result exists: ${browseResult?.result != null} ===") | 33 | ec.logger.info("=== browseResult: ${browseResult != null}, result exists: ${browseResult?.result != null} ===") | ... | ... |
| ... | @@ -80,7 +80,7 @@ class UiNarrativeBuilder { | ... | @@ -80,7 +80,7 @@ class UiNarrativeBuilder { |
| 80 | 80 | ||
| 81 | def forms = semanticState?.data | 81 | def forms = semanticState?.data |
| 82 | if (forms) { | 82 | if (forms) { |
| 83 | def maxForms = isTerse ? 2 : 10 | 83 | def maxForms = 10 |
| 84 | def formNames = forms.keySet().findAll { k -> k.contains('Form') || k.contains('form') } | 84 | def formNames = forms.keySet().findAll { k -> k.contains('Form') || k.contains('form') } |
| 85 | if (formNames) { | 85 | if (formNames) { |
| 86 | def formNamesToDescribe = formNames.take(maxForms + 1) | 86 | def formNamesToDescribe = formNames.take(maxForms + 1) |
| ... | @@ -95,7 +95,7 @@ class UiNarrativeBuilder { | ... | @@ -95,7 +95,7 @@ class UiNarrativeBuilder { |
| 95 | if (links && links.size() > 0) { | 95 | if (links && links.size() > 0) { |
| 96 | def linkTypes = links.collect { l -> l.type?.toString() ?: 'navigation' }.unique() | 96 | def linkTypes = links.collect { l -> l.type?.toString() ?: 'navigation' }.unique() |
| 97 | if (linkTypes) { | 97 | if (linkTypes) { |
| 98 | def maxTypes = isTerse ? 3 : 15 | 98 | def maxTypes = 15 |
| 99 | sb.append("Available links: ${linkTypes.take(maxTypes).join(', ')}. ") | 99 | sb.append("Available links: ${linkTypes.take(maxTypes).join(', ')}. ") |
| 100 | } | 100 | } |
| 101 | } | 101 | } |
| ... | @@ -168,7 +168,7 @@ class UiNarrativeBuilder { | ... | @@ -168,7 +168,7 @@ class UiNarrativeBuilder { |
| 168 | if (links && links.size() > 0) { | 168 | if (links && links.size() > 0) { |
| 169 | def sortedLinks = links.sort { a, b -> (a.text <=> b.text) } | 169 | def sortedLinks = links.sort { a, b -> (a.text <=> b.text) } |
| 170 | 170 | ||
| 171 | def linksToTake = isTerse ? 5 : 50 | 171 | def linksToTake = 50 |
| 172 | sortedLinks.take(linksToTake).each { link -> | 172 | sortedLinks.take(linksToTake).each { link -> |
| 173 | def linkText = link.text?.toString() | 173 | def linkText = link.text?.toString() |
| 174 | def linkPath = link.path?.toString() | 174 | def linkPath = link.path?.toString() | ... | ... |
-
Please register or sign in to post a comment