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
Showing
9 changed files
with
120 additions
and
16 deletions
| 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)) | ... | ... |
This diff is collapsed.
Click to expand it.
| ... | @@ -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) { | ... | ... |
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
| ... | @@ -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 = [:]) { | ... | ... |
-
Please register or sign in to post a comment