03e420e8 by Ean Schuessler

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
1 parent 7c7abe25
...@@ -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>
169 <#if optionCount lt optionLimit>
164 <#assign optLabel = (dropdownOptions.get(optKey))!optKey> 170 <#assign optLabel = (dropdownOptions.get(optKey))!optKey>
165 <#assign optionsList = optionsList + [{"value": optKey, "label": optLabel}]> 171 <#assign optionsList = optionsList + [{"value": optKey, "label": optLabel}]>
172 </#if>
173 <#assign optionCount = optionCount + 1>
166 </#list> 174 </#list>
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>
167 <#assign fieldMeta = fieldMeta + {"type": "dropdown", "options": optionsList}> 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>
304 <#if optionCount lt optionLimit>
287 <#assign optionsList = optionsList + [{"value": optKey, "label": dropdownOptions[optKey]!optKey}]> 305 <#assign optionsList = optionsList + [{"value": optKey, "label": dropdownOptions[optKey]!optKey}]>
306 </#if>
307 <#assign optionCount = optionCount + 1>
288 </#list> 308 </#list>
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>
289 <#assign fieldMeta = fieldMeta + {"type": "dropdown", "options": optionsList}> 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()
......