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
1 arguments=--init-script /home/ean/.local/share/opencode/bin/jdtls/config_linux/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle 1 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
2 auto.sync=false 2 auto.sync=false
3 build.scans.enabled=false 3 build.scans.enabled=false
4 connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.9)) 4 connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.9))
......
...@@ -17,7 +17,6 @@ warranty. ...@@ -17,7 +17,6 @@ warranty.
17 <parameter name="parameters" type="Map"/> 17 <parameter name="parameters" type="Map"/>
18 <parameter name="renderMode" default="mcp"/> 18 <parameter name="renderMode" default="mcp"/>
19 <parameter name="sessionId"/> 19 <parameter name="sessionId"/>
20 <parameter name="terse" type="Boolean" default="false"/>
21 </in-parameters> 20 </in-parameters>
22 <out-parameters> 21 <out-parameters>
23 <parameter name="result" type="Map"/> 22 <parameter name="result" type="Map"/>
...@@ -48,8 +47,7 @@ warranty. ...@@ -48,8 +47,7 @@ warranty.
48 path: screenPath, 47 path: screenPath,
49 parameters: parameters ?: [:], 48 parameters: parameters ?: [:],
50 renderMode: renderMode ?: "mcp", 49 renderMode: renderMode ?: "mcp",
51 sessionId: sessionId, 50 sessionId: sessionId
52 terse: terse == true
53 ] 51 ]
54 if (subscreenName) screenCallParams.subscreenName = subscreenName 52 if (subscreenName) screenCallParams.subscreenName = subscreenName
55 53
...@@ -86,8 +84,7 @@ warranty. ...@@ -86,8 +84,7 @@ warranty.
86 uiNarrative = narrativeBuilder.buildNarrative( 84 uiNarrative = narrativeBuilder.buildNarrative(
87 screenDefForNarrative, 85 screenDefForNarrative,
88 semanticState, 86 semanticState,
89 path, 87 path
90 terse == true
91 ) 88 )
92 ec.logger.info("RenderScreenNarrative: Generated UI narrative for ${path}") 89 ec.logger.info("RenderScreenNarrative: Generated UI narrative for ${path}")
93 } catch (Exception e) { 90 } catch (Exception e) {
......
...@@ -46,18 +46,18 @@ class UiNarrativeBuilder { ...@@ -46,18 +46,18 @@ class UiNarrativeBuilder {
46 return count 46 return count
47 } 47 }
48 48
49 Map<String, Object> buildNarrative(ScreenDefinition screenDef, Map<String, Object> semanticState, String currentPath, boolean isTerse) { 49 Map<String, Object> buildNarrative(ScreenDefinition screenDef, Map<String, Object> semanticState, String currentPath) {
50 def narrative = [:] 50 def narrative = [:]
51 51
52 narrative.screen = describeScreen(screenDef, semanticState, isTerse) 52 narrative.screen = describeScreen(screenDef, semanticState)
53 narrative.actions = describeActions(screenDef, semanticState, currentPath, isTerse) 53 narrative.actions = describeActions(screenDef, semanticState, currentPath)
54 narrative.navigation = describeLinks(semanticState, currentPath, isTerse) 54 narrative.navigation = describeLinks(semanticState, currentPath)
55 narrative.notes = describeNotes(semanticState, currentPath, isTerse) 55 narrative.notes = describeNotes(semanticState, currentPath)
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) {
61 def screenName = screenDef?.getScreenName() ?: "Screen" 61 def screenName = screenDef?.getScreenName() ?: "Screen"
62 def sb = new StringBuilder() 62 def sb = new StringBuilder()
63 63
...@@ -103,7 +103,7 @@ class UiNarrativeBuilder { ...@@ -103,7 +103,7 @@ class UiNarrativeBuilder {
103 return sb.toString() 103 return sb.toString()
104 } 104 }
105 105
106 List<String> describeActions(ScreenDefinition screenDef, Map<String, Object> semanticState, String currentPath, boolean isTerse) { 106 List<String> describeActions(ScreenDefinition screenDef, Map<String, Object> semanticState, String currentPath) {
107 def actions = [] 107 def actions = []
108 108
109 def transitions = semanticState?.actions 109 def transitions = semanticState?.actions
...@@ -161,7 +161,7 @@ class UiNarrativeBuilder { ...@@ -161,7 +161,7 @@ class UiNarrativeBuilder {
161 return 'screen-transition' 161 return 'screen-transition'
162 } 162 }
163 163
164 List<String> describeLinks(Map<String, Object> semanticState, String currentPath, boolean isTerse) { 164 List<String> describeLinks(Map<String, Object> semanticState, String currentPath) {
165 def navigation = [] 165 def navigation = []
166 166
167 def links = semanticState?.data?.links 167 def links = semanticState?.data?.links
...@@ -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, String currentPath, boolean isTerse) { 204 List<String> describeNotes(Map<String, Object> semanticState, String currentPath) {
205 def notes = [] 205 def notes = []
206 206
207 def data = semanticState?.data 207 def data = semanticState?.data
...@@ -210,7 +210,7 @@ class UiNarrativeBuilder { ...@@ -210,7 +210,7 @@ class UiNarrativeBuilder {
210 if (value instanceof Map && value.containsKey('_truncated') && value._truncated == true) { 210 if (value instanceof Map && value.containsKey('_truncated') && value._truncated == true) {
211 def total = value._totalCount ?: 0 211 def total = value._totalCount ?: 0
212 def shown = value._items?.size() ?: 0 212 def shown = value._items?.size() ?: 0
213 notes << "List truncated: showing ${shown} of ${total} item${total > 1 ? 's' : ''}. Set terse=false to view all." 213 notes << "List truncated: showing ${shown} of ${total} item${total > 1 ? 's' : ''}. Use pagination for more."
214 } 214 }
215 } 215 }
216 } 216 }
......
1 /*
2 * This software is in the public domain under CC0 1.0 Universal plus a
3 * Grant of Patent License.
4 */
5 package org.moqui.mcp.test
6
7 import org.moqui.Moqui
8 import org.moqui.context.ExecutionContext
9 import spock.lang.Shared
10 import spock.lang.Specification
11 import spock.lang.Stepwise
12
13 @Stepwise
14 class AutocompleteTest extends Specification {
15 @Shared ExecutionContext ec
16 @Shared SimpleMcpClient client
17
18 def setupSpec() {
19 ec = Moqui.getExecutionContext()
20 client = new SimpleMcpClient()
21 client.initializeSession()
22
23 // Log in to ensure permissions
24 ec.user.internalLoginUser("john.doe")
25 }
26
27 def cleanupSpec() {
28 if (client) client.closeSession()
29 if (ec) ec.destroy()
30 }
31
32 def "Test getCategoryList Autocomplete"() {
33 when:
34 println "🔍 Testing getCategoryList autocomplete on Search screen"
35
36 // 1. Get screen details to find the field and transition
37 def details = client.callTool("moqui_get_screen_details", [
38 path: "component://SimpleScreens/screen/SimpleScreens/Catalog/Search.xml",
39 fieldName: "productCategoryId"
40 ])
41
42 println "📋 Screen details result: ${details}"
43
44 then:
45 details != null
46 !details.error
47 !details.result?.error
48
49 def content = details.result?.content
50 content != null
51 content.size() > 0
52
53 // Parse the text content from the response
54 def jsonText = content[0].text
55 def jsonResult = new groovy.json.JsonSlurper().parseText(jsonText)
56
57 def field = jsonResult.fields?.productCategoryId
58 field != null
59 field.dynamicOptions != null
60 field.dynamicOptions.transition == "getCategoryList"
61 field.dynamicOptions.serverSearch == true
62
63 println "✅ Field metadata verified: ${field.dynamicOptions}"
64
65 when:
66 println "🔍 Testing explicit transition call via TransitionAsMcpTool"
67
68 // 2. Call the transition directly to simulate typing
69 def transitionResult = ec.service.sync().name("McpServices.execute#TransitionAsMcpTool")
70 .parameters([
71 path: "component://SimpleScreens/screen/SimpleScreens/Catalog/Search.xml",
72 transitionName: "getCategoryList",
73 parameters: [term: ""]
74 ])
75 .call()
76
77 println "🔄 Transition result: ${transitionResult}"
78
79 then:
80 transitionResult != null
81 transitionResult.result != null
82 !transitionResult.result.error
83 transitionResult.result.data != null
84 transitionResult.result.data instanceof List
85 transitionResult.result.data.size() > 0
86
87 println "✅ Found ${transitionResult.result.data.size()} categories via transition"
88 println "📝 First category: ${transitionResult.result.data[0]}"
89 }
90 }
...@@ -155,6 +155,23 @@ class SimpleMcpClient { ...@@ -155,6 +155,23 @@ class SimpleMcpClient {
155 } 155 }
156 156
157 /** 157 /**
158 * Call any tool
159 */
160 Map callTool(String toolName, Map arguments = [:]) {
161 try {
162 def result = makeJsonRpcRequest("tools/call", [
163 name: toolName,
164 arguments: arguments
165 ])
166
167 return result ?: [error: [message: "No response from server"]]
168 } catch (Exception e) {
169 println "Error calling tool ${toolName}: ${e.message}"
170 return [error: [message: e.message]]
171 }
172 }
173
174 /**
158 * Call a screen tool 175 * Call a screen tool
159 */ 176 */
160 Map callScreen(String screenPath, Map parameters = [:]) { 177 Map callScreen(String screenPath, Map parameters = [:]) {
......