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 @@
<#assign dropdownOptions = sri.getFieldOptions(dropdownNode)!>
<#if (dropdownOptions?size!0) gt 0>
<#-- Build options list from the LinkedHashMap -->
<#-- Truncate if > 10 unless mcpFullOptions is set (for get_screen_details) -->
<#assign optionsList = []>
<#assign totalOptions = dropdownOptions?size>
<#assign skipTruncation = (ec.context.mcpFullOptions!false) == true>
<#assign optionLimit = skipTruncation?then(999999, 10)>
<#assign optionCount = 0>
<#list (dropdownOptions.keySet())! as optKey>
<#assign optLabel = (dropdownOptions.get(optKey))!optKey>
<#assign optionsList = optionsList + [{"value": optKey, "label": optLabel}]>
<#if optionCount lt optionLimit>
<#assign optLabel = (dropdownOptions.get(optKey))!optKey>
<#assign optionsList = optionsList + [{"value": optKey, "label": optLabel}]>
</#if>
<#assign optionCount = optionCount + 1>
</#list>
<#assign fieldMeta = fieldMeta + {"type": "dropdown", "options": optionsList}>
<#if (totalOptions gt 10) && !skipTruncation>
<#assign fieldMeta = fieldMeta + {"type": "dropdown", "options": optionsList, "optionsTruncated": true, "totalOptions": totalOptions, "fetchHint": "Use moqui_get_screen_details(fieldName='" + (fieldNode["@name"]!"") + "') for all " + totalOptions + " options"}>
<#else>
<#assign fieldMeta = fieldMeta + {"type": "dropdown", "options": optionsList}>
</#if>
<#else>
<#-- No static options - check for dynamic-options -->
<#assign dynamicOptionsList = dropdownNode["dynamic-options"]!>
......@@ -281,12 +293,24 @@
</#list>
<#assign dropdownOptions = sri.getFieldOptions(fieldSubNode)!>
<#if dropdownOptions?has_content && dropdownOptions?size gt 0>
<#-- Convert LinkedHashMap<String,String> to list of {value, label} objects for JSON -->
<#-- Convert LinkedHashMap<String,String> to list of {value, label} objects -->
<#-- Truncate if > 10 unless mcpFullOptions is set (for get_screen_details) -->
<#assign optionsList = []>
<#assign totalOptions = dropdownOptions?size>
<#assign skipTruncation = (ec.context.mcpFullOptions!false) == true>
<#assign optionLimit = skipTruncation?then(999999, 10)>
<#assign optionCount = 0>
<#list dropdownOptions?keys as optKey>
<#assign optionsList = optionsList + [{"value": optKey, "label": dropdownOptions[optKey]!optKey}]>
<#if optionCount lt optionLimit>
<#assign optionsList = optionsList + [{"value": optKey, "label": dropdownOptions[optKey]!optKey}]>
</#if>
<#assign optionCount = optionCount + 1>
</#list>
<#assign fieldMeta = fieldMeta + {"type": "dropdown", "options": optionsList}>
<#if (totalOptions gt 10) && !skipTruncation>
<#assign fieldMeta = fieldMeta + {"type": "dropdown", "options": optionsList, "optionsTruncated": true, "totalOptions": totalOptions, "fetchHint": "Use moqui_get_screen_details(fieldName='" + (fieldNode["@name"]!"") + "') for all " + totalOptions + " options"}>
<#else>
<#assign fieldMeta = fieldMeta + {"type": "dropdown", "options": optionsList}>
</#if>
<#else>
<#assign dropdownNode = fieldSubNode["drop-down"]!>
......
......@@ -601,6 +601,21 @@ def getSimplePath = { fullPath ->
return parts.join('/')
}
// Helper to extract short description/summary from wiki content
def extractSummary = { wikiText ->
if (!wikiText) return null
def textString = wikiText instanceof String ? wikiText : new String(wikiText, "UTF-8")
def lines = textString.split('\n')
for (def line : lines) {
def trimmed = line.trim()
// Skip empty lines and headers
if (trimmed && !trimmed.startsWith('#')) {
return trimmed.take(200)
}
}
return null
}
// Helper function to load wiki instructions for a screen
def getWikiInstructions = { lookupPath ->
try {
......@@ -1061,7 +1076,11 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
if (output) {
mcpResult.textPreview = output.take(2000) + (output.length() > 2000 ? "..." : "")
}
if (wikiInstructions) mcpResult.wikiInstructions = wikiInstructions
if (wikiInstructions) {
mcpResult.wikiInstructions = wikiInstructions
def summary = extractSummary(wikiInstructions)
if (summary) mcpResult.summary = summary
}
content << [
type: "text",
......@@ -1781,6 +1800,11 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
if (wikiInstructions) {
resultMap.wikiInstructions = wikiInstructions
// Extract first non-header paragraph as summary
def summary = getShortDescription(wikiInstructions)
if (summary) {
resultMap.summary = summary
}
}
if (renderError) {
......
......@@ -23,8 +23,11 @@ class McpFieldOptionsService {
def result = [screenPath: path, fields: [:]]
try {
// Pass mcpFullOptions through parameters to get full dropdown options without truncation
def mergedParams = (parameters ?: [:]) + [mcpFullOptions: true]
def browseResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool")
.parameters([path: path, parameters: parameters ?: [:], renderMode: "mcp", sessionId: null])
.parameters([path: path, parameters: mergedParams, renderMode: "mcp", sessionId: null])
.call()
ec.logger.info("=== browseResult: ${browseResult != null}, result exists: ${browseResult?.result != null} ===")
......
......@@ -80,7 +80,7 @@ class UiNarrativeBuilder {
def forms = semanticState?.data
if (forms) {
def maxForms = isTerse ? 2 : 10
def maxForms = 10
def formNames = forms.keySet().findAll { k -> k.contains('Form') || k.contains('form') }
if (formNames) {
def formNamesToDescribe = formNames.take(maxForms + 1)
......@@ -95,7 +95,7 @@ class UiNarrativeBuilder {
if (links && links.size() > 0) {
def linkTypes = links.collect { l -> l.type?.toString() ?: 'navigation' }.unique()
if (linkTypes) {
def maxTypes = isTerse ? 3 : 15
def maxTypes = 15
sb.append("Available links: ${linkTypes.take(maxTypes).join(', ')}. ")
}
}
......@@ -168,7 +168,7 @@ class UiNarrativeBuilder {
if (links && links.size() > 0) {
def sortedLinks = links.sort { a, b -> (a.text <=> b.text) }
def linksToTake = isTerse ? 5 : 50
def linksToTake = 50
sortedLinks.take(linksToTake).each { link ->
def linkText = link.text?.toString()
def linkPath = link.path?.toString()
......