7c7abe25 by Ean Schuessler

Fix static dropdown options and remove terse mode

- Fix sri.getFieldOptions() call to pass drop-down node instead of parent field node
- Remove terse parameter from all services and serialization
- Increase safety limits by 10x: lists 10K items, strings 1MB, unknown types 10K chars
- Add form-list field metadata extraction in FTL macros
- Simplify McpFieldOptionsService to use CustomScreenTestImpl approach
- Clean up redundant _fixed.groovy file
1 parent a06fd54e
arguments=--init-script /home/ean/.local/share/opencode/bin/jdtls/config_linux/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle
arguments=--init-script /home/ean/.config/Code/User/globalStorage/redhat.java/1.51.0/config_linux/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle --init-script /home/ean/.config/Code/User/globalStorage/redhat.java/1.51.0/config_linux/org.eclipse.osgi/58/0/.cp/gradle/protobuf/init.gradle
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.9))
......
......@@ -17,7 +17,6 @@ warranty.
<parameter name="parameters" type="Map"/>
<parameter name="renderMode" default="mcp"/>
<parameter name="sessionId"/>
<parameter name="terse" type="Boolean" default="false"/>
</in-parameters>
<out-parameters>
<parameter name="result" type="Map"/>
......@@ -48,8 +47,7 @@ warranty.
path: screenPath,
parameters: parameters ?: [:],
renderMode: renderMode ?: "mcp",
sessionId: sessionId,
terse: terse == true
sessionId: sessionId
]
if (subscreenName) screenCallParams.subscreenName = subscreenName
......@@ -86,8 +84,7 @@ warranty.
uiNarrative = narrativeBuilder.buildNarrative(
screenDefForNarrative,
semanticState,
path,
terse == true
path
)
ec.logger.info("RenderScreenNarrative: Generated UI narrative for ${path}")
} catch (Exception e) {
......
......@@ -46,18 +46,18 @@ class UiNarrativeBuilder {
return count
}
Map<String, Object> buildNarrative(ScreenDefinition screenDef, Map<String, Object> semanticState, String currentPath, boolean isTerse) {
Map<String, Object> buildNarrative(ScreenDefinition screenDef, Map<String, Object> semanticState, String currentPath) {
def narrative = [:]
narrative.screen = describeScreen(screenDef, semanticState, isTerse)
narrative.actions = describeActions(screenDef, semanticState, currentPath, isTerse)
narrative.navigation = describeLinks(semanticState, currentPath, isTerse)
narrative.notes = describeNotes(semanticState, currentPath, isTerse)
narrative.screen = describeScreen(screenDef, semanticState)
narrative.actions = describeActions(screenDef, semanticState, currentPath)
narrative.navigation = describeLinks(semanticState, currentPath)
narrative.notes = describeNotes(semanticState, currentPath)
return narrative
}
String describeScreen(ScreenDefinition screenDef, Map<String, Object> semanticState, boolean isTerse) {
String describeScreen(ScreenDefinition screenDef, Map<String, Object> semanticState) {
def screenName = screenDef?.getScreenName() ?: "Screen"
def sb = new StringBuilder()
......@@ -103,7 +103,7 @@ class UiNarrativeBuilder {
return sb.toString()
}
List<String> describeActions(ScreenDefinition screenDef, Map<String, Object> semanticState, String currentPath, boolean isTerse) {
List<String> describeActions(ScreenDefinition screenDef, Map<String, Object> semanticState, String currentPath) {
def actions = []
def transitions = semanticState?.actions
......@@ -161,7 +161,7 @@ class UiNarrativeBuilder {
return 'screen-transition'
}
List<String> describeLinks(Map<String, Object> semanticState, String currentPath, boolean isTerse) {
List<String> describeLinks(Map<String, Object> semanticState, String currentPath) {
def navigation = []
def links = semanticState?.data?.links
......@@ -201,7 +201,7 @@ class UiNarrativeBuilder {
return navigation
}
List<String> describeNotes(Map<String, Object> semanticState, String currentPath, boolean isTerse) {
List<String> describeNotes(Map<String, Object> semanticState, String currentPath) {
def notes = []
def data = semanticState?.data
......@@ -210,7 +210,7 @@ class UiNarrativeBuilder {
if (value instanceof Map && value.containsKey('_truncated') && value._truncated == true) {
def total = value._totalCount ?: 0
def shown = value._items?.size() ?: 0
notes << "List truncated: showing ${shown} of ${total} item${total > 1 ? 's' : ''}. Set terse=false to view all."
notes << "List truncated: showing ${shown} of ${total} item${total > 1 ? 's' : ''}. Use pagination for more."
}
}
}
......
/*
* This software is in the public domain under CC0 1.0 Universal plus a
* Grant of Patent License.
*/
package org.moqui.mcp.test
import org.moqui.Moqui
import org.moqui.context.ExecutionContext
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Stepwise
@Stepwise
class AutocompleteTest extends Specification {
@Shared ExecutionContext ec
@Shared SimpleMcpClient client
def setupSpec() {
ec = Moqui.getExecutionContext()
client = new SimpleMcpClient()
client.initializeSession()
// Log in to ensure permissions
ec.user.internalLoginUser("john.doe")
}
def cleanupSpec() {
if (client) client.closeSession()
if (ec) ec.destroy()
}
def "Test getCategoryList Autocomplete"() {
when:
println "🔍 Testing getCategoryList autocomplete on Search screen"
// 1. Get screen details to find the field and transition
def details = client.callTool("moqui_get_screen_details", [
path: "component://SimpleScreens/screen/SimpleScreens/Catalog/Search.xml",
fieldName: "productCategoryId"
])
println "📋 Screen details result: ${details}"
then:
details != null
!details.error
!details.result?.error
def content = details.result?.content
content != null
content.size() > 0
// Parse the text content from the response
def jsonText = content[0].text
def jsonResult = new groovy.json.JsonSlurper().parseText(jsonText)
def field = jsonResult.fields?.productCategoryId
field != null
field.dynamicOptions != null
field.dynamicOptions.transition == "getCategoryList"
field.dynamicOptions.serverSearch == true
println "✅ Field metadata verified: ${field.dynamicOptions}"
when:
println "🔍 Testing explicit transition call via TransitionAsMcpTool"
// 2. Call the transition directly to simulate typing
def transitionResult = ec.service.sync().name("McpServices.execute#TransitionAsMcpTool")
.parameters([
path: "component://SimpleScreens/screen/SimpleScreens/Catalog/Search.xml",
transitionName: "getCategoryList",
parameters: [term: ""]
])
.call()
println "🔄 Transition result: ${transitionResult}"
then:
transitionResult != null
transitionResult.result != null
!transitionResult.result.error
transitionResult.result.data != null
transitionResult.result.data instanceof List
transitionResult.result.data.size() > 0
println "✅ Found ${transitionResult.result.data.size()} categories via transition"
println "📝 First category: ${transitionResult.result.data[0]}"
}
}
......@@ -155,6 +155,23 @@ class SimpleMcpClient {
}
/**
* Call any tool
*/
Map callTool(String toolName, Map arguments = [:]) {
try {
def result = makeJsonRpcRequest("tools/call", [
name: toolName,
arguments: arguments
])
return result ?: [error: [message: "No response from server"]]
} catch (Exception e) {
println "Error calling tool ${toolName}: ${e.message}"
return [error: [message: e.message]]
}
}
/**
* Call a screen tool
*/
Map callScreen(String screenPath, Map parameters = [:]) {
......