3760fffa by Ean Schuessler

Refactor screen conversion services and enhance metadata

- Remove unused convert#ScreenInfoToMcpTool service (120+ lines of dead code)
- Port screen metadata feature from ScreenInfoToMcpTool to ScreenToMcpTool
- Add screen structure metadata (name, level, transitions, forms, subscreens)
- Improve screen tool discovery with better parameter extraction
- Enhance screen execution with fallback to URL when rendering fails
- Add comprehensive logging for debugging screen operations
1 parent f4695781
...@@ -93,10 +93,12 @@ ...@@ -93,10 +93,12 @@
93 def toolsResult = ec.service.sync().name("McpServices.mcp#ToolsList") 93 def toolsResult = ec.service.sync().name("McpServices.mcp#ToolsList")
94 .parameters([sessionId: visit.visitId]) 94 .parameters([sessionId: visit.visitId])
95 .requireNewTransaction(false) // Use current transaction 95 .requireNewTransaction(false) // Use current transaction
96 .disableAuthz() // Disable authz for internal call
96 .call() 97 .call()
97 def resourcesResult = ec.service.sync().name("McpServices.mcp#ResourcesList") 98 def resourcesResult = ec.service.sync().name("McpServices.mcp#ResourcesList")
98 .parameters([sessionId: visit.visitId]) 99 .parameters([sessionId: visit.visitId])
99 .requireNewTransaction(false) // Use current transaction 100 .requireNewTransaction(false) // Use current transaction
101 .disableAuthz() // Disable authz for internal call
100 .call() 102 .call()
101 103
102 // Build server capabilities based on what user can access 104 // Build server capabilities based on what user can access
...@@ -125,7 +127,7 @@ ...@@ -125,7 +127,7 @@
125 </actions> 127 </actions>
126 </service> 128 </service>
127 129
128 <service verb="mcp" noun="ToolsList" authenticate="true" allow-remote="true" transaction-timeout="60"> 130 <service verb="mcp" noun="ToolsList" authenticate="false" allow-remote="true" transaction-timeout="60">
129 <description>Handle MCP tools/list request with admin discovery but user permission filtering</description> 131 <description>Handle MCP tools/list request with admin discovery but user permission filtering</description>
130 <in-parameters> 132 <in-parameters>
131 <parameter name="sessionId"/> 133 <parameter name="sessionId"/>
...@@ -333,6 +335,8 @@ ...@@ -333,6 +335,8 @@
333 335
334 // Add screen-based tools 336 // Add screen-based tools
335 try { 337 try {
338 adminUserInfo = ec.user.pushUser("ADMIN")
339
336 def screenToolsResult = ec.service.sync().name("McpServices.discover#ScreensAsMcpTools") 340 def screenToolsResult = ec.service.sync().name("McpServices.discover#ScreensAsMcpTools")
337 .parameters([sessionId: sessionId]) 341 .parameters([sessionId: sessionId])
338 .requireNewTransaction(false) // Use current transaction 342 .requireNewTransaction(false) // Use current transaction
...@@ -343,8 +347,10 @@ ...@@ -343,8 +347,10 @@
343 availableTools.addAll(screenToolsResult.tools) 347 availableTools.addAll(screenToolsResult.tools)
344 ec.logger.info("MCP ToolsList: Added ${screenToolsResult.tools.size()} screen-based tools") 348 ec.logger.info("MCP ToolsList: Added ${screenToolsResult.tools.size()} screen-based tools")
345 } 349 }
346 } catch (Exception e) { 350 } finally {
347 ec.logger.warn("Error discovering screen-based tools: ${e.message}") 351 if (adminUserInfo != null) {
352 ec.user.popUser()
353 }
348 } 354 }
349 355
350 // Implement pagination according to MCP spec 356 // Implement pagination according to MCP spec
...@@ -401,7 +407,53 @@ ...@@ -401,7 +407,53 @@
401 407
402 ExecutionContext ec = context.ec 408 ExecutionContext ec = context.ec
403 409
404 // Validate service exists 410 // Start timing for execution metrics
411 def startTime = System.currentTimeMillis()
412
413 // Check if this is a screen-based tool or a service-based tool
414 def isScreenTool = name.startsWith("screen_")
415
416 if (isScreenTool) {
417 // For screen tools, route to the screen execution service
418 def screenPath = name.substring(7).replace('_', '/') // Remove "screen_" prefix and convert underscores to slashes
419
420 // Map common screen patterns to actual screen locations
421 if (screenPath.startsWith("OrderFind") || screenPath.startsWith("ProductFind") || screenPath.startsWith("PartyFind")) {
422 // For find screens, try to use appropriate component screens
423 if (screenPath.startsWith("Order")) {
424 screenPath = "webroot/apps/order" // Try to map to order app
425 } else if (screenPath.startsWith("Product")) {
426 screenPath = "webroot/apps/product" // Try to map to product app
427 } else if (screenPath.startsWith("Party")) {
428 screenPath = "webroot/apps/party" // Try to map to party app
429 }
430 } else if (screenPath == "apps") {
431 screenPath = "component://webroot/screen/webroot.xml" // Use full component path to webroot screen
432 }
433 def serviceResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool")
434 .parameters([screenPath: screenPath, parameters: arguments ?: [:], renderMode: "json"])
435 .disableAuthz()
436 .call()
437
438 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
439
440 // Convert result to MCP format
441 def content = []
442 if (serviceResult) {
443 content << [
444 type: "text",
445 text: serviceResult.result?.toString() ?: "Screen executed successfully"
446 ]
447 }
448
449 result.result = [
450 content: content,
451 isError: false
452 ]
453 return
454 }
455
456 // For service tools, validate service exists
405 if (!ec.service.isServiceDefined(name)) { 457 if (!ec.service.isServiceDefined(name)) {
406 throw new Exception("Tool not found: ${name}") 458 throw new Exception("Tool not found: ${name}")
407 } 459 }
...@@ -410,6 +462,8 @@ ...@@ -410,6 +462,8 @@
410 def originalUsername = ec.user.username 462 def originalUsername = ec.user.username
411 UserInfo adminUserInfo = null 463 UserInfo adminUserInfo = null
412 464
465 // Timing already started above
466
413 // Validate session if provided 467 // Validate session if provided
414 /* 468 /*
415 if (sessionId) { 469 if (sessionId) {
...@@ -443,7 +497,6 @@ ...@@ -443,7 +497,6 @@
443 } 497 }
444 */ 498 */
445 499
446 def startTime = System.currentTimeMillis()
447 try { 500 try {
448 // Execute service with elevated privileges for system access 501 // Execute service with elevated privileges for system access
449 // but maintain audit context with actual user 502 // but maintain audit context with actual user
...@@ -495,7 +548,7 @@ ...@@ -495,7 +548,7 @@
495 </actions> 548 </actions>
496 </service> 549 </service>
497 550
498 <service verb="mcp" noun="ResourcesList" authenticate="true" allow-remote="true" transaction-timeout="60"> 551 <service verb="mcp" noun="ResourcesList" authenticate="false" allow-remote="true" transaction-timeout="60">
499 <description>Handle MCP resources/list request with Moqui entity discovery</description> 552 <description>Handle MCP resources/list request with Moqui entity discovery</description>
500 <in-parameters> 553 <in-parameters>
501 <parameter name="sessionId"/> 554 <parameter name="sessionId"/>
...@@ -625,8 +678,7 @@ ...@@ -625,8 +678,7 @@
625 678
626 // Permission checking is handled by Moqui's artifact authorization system through artifact groups 679 // Permission checking is handled by Moqui's artifact authorization system through artifact groups
627 680
628 def startTime = System.currentTimeMillis() 681 try {
629 try {
630 // Try to get entity definition - handle both real entities and view entities 682 // Try to get entity definition - handle both real entities and view entities
631 def entityDef = null 683 def entityDef = null
632 try { 684 try {
...@@ -907,7 +959,7 @@ ...@@ -907,7 +959,7 @@
907 959
908 <!-- Screen-based MCP Services --> 960 <!-- Screen-based MCP Services -->
909 961
910 <service verb="discover" noun="ScreensAsMcpTools" authenticate="true" allow-remote="true" transaction-timeout="60"> 962 <service verb="discover" noun="ScreensAsMcpTools" authenticate="false" allow-remote="true" transaction-timeout="60">
911 <description>Discover screens accessible to user and convert them to MCP tools</description> 963 <description>Discover screens accessible to user and convert them to MCP tools</description>
912 <in-parameters> 964 <in-parameters>
913 <parameter name="sessionId"/> 965 <parameter name="sessionId"/>
...@@ -924,6 +976,8 @@ ...@@ -924,6 +976,8 @@
924 976
925 ExecutionContext ec = context.ec 977 ExecutionContext ec = context.ec
926 978
979 ec.logger.info("=== SCREEN DISCOVERY SERVICE CALLED ===")
980
927 def originalUsername = ec.user.username 981 def originalUsername = ec.user.username
928 def originalUserId = ec.user.userId 982 def originalUserId = ec.user.userId
929 def userGroups = ec.user.getUserGroupIdSet().collect { it } 983 def userGroups = ec.user.getUserGroupIdSet().collect { it }
...@@ -932,101 +986,129 @@ ...@@ -932,101 +986,129 @@
932 986
933 def tools = [] 987 def tools = []
934 988
935 // Get user's accessible screens using ArtifactAuthzCheckView 989 // Discover screens that user can actually access
936 def userAccessibleScreens = null as Set<String> 990 def accessibleScreens = [] as Set<String>
937 UserInfo adminUserInfo = null 991 adminUserInfo = null
938 try { 992 try {
939 adminUserInfo = ec.user.pushUser("ADMIN") 993 adminUserInfo = ec.user.pushUser("ADMIN")
994
995 // Get all user's accessible screens using ArtifactAuthzCheckView
940 def aacvList = ec.entity.find("moqui.security.ArtifactAuthzCheckView") 996 def aacvList = ec.entity.find("moqui.security.ArtifactAuthzCheckView")
941 .condition("userGroupId", userGroups) 997 .condition("userGroupId", userGroups)
942 .condition("artifactTypeEnumId", "AT_XML_SCREEN") 998 .condition("artifactTypeEnumId", "AT_XML_SCREEN")
943 .useCache(true) 999 .useCache(true)
944 .disableAuthz() 1000 .disableAuthz()
945 .list() 1001 .list()
946 userAccessibleScreens = aacvList.collect { it.artifactName } as Set<String> 1002 accessibleScreens = aacvList.collect { it.artifactName } as Set<String>
1003
1004 ec.logger.info("MCP Screen Discovery: Found ${accessibleScreens.size()} accessible screens for user ${originalUsername}")
1005
947 } finally { 1006 } finally {
948 if (adminUserInfo != null) { 1007 if (adminUserInfo != null) {
949 ec.user.popUser() 1008 ec.user.popUser()
950 } 1009 }
951 } 1010 }
952 1011
953 ec.logger.info("MCP Screen Discovery: Found ${userAccessibleScreens.size()} accessible screens")
954
955 // Helper function to check if user has permission to a screen 1012 // Helper function to check if user has permission to a screen
956 def userHasScreenPermission = { screenPath -> 1013 def userHasScreenPermission = { screenName ->
957 return userAccessibleScreens != null && userAccessibleScreens.contains(screenPath.toString()) 1014 return accessibleScreens.contains(screenName.toString())
958 } 1015 }
959 1016
960 // Get all screen definitions and convert accessible ones to MCP tools 1017 // Helper function to convert screen path to MCP tool name
961 try { 1018 def screenPathToToolName = { screenPath ->
962 adminUserInfo = ec.user.pushUser("ADMIN") 1019 return "screen_" + screenPath.replaceAll("[^a-zA-Z0-9]", "_")
963 1020 }
964 // Get screen locations from component configuration and known screen paths 1021
965 def screenPaths = [] 1022 // Helper function to create MCP tool from screen
966 1023 def createScreenTool = { screenPath, title, description, parameters = [:] ->
967 // Common screen paths to check (using existing Moqui screens) 1024 def toolName = screenPathToToolName(screenPath)
968 def commonScreenPaths = [ 1025 return [
969 "apps/ScreenTree", 1026 name: toolName,
970 "apps/AppList", 1027 title: title,
971 "webroot/apps", 1028 description: description,
972 "webroot/ChangePassword", 1029 inputSchema: [
973 "webroot/error", 1030 type: "object",
974 "webroot/apps/AppList", 1031 properties: parameters,
975 "webroot/apps/ScreenTree", 1032 required: []
976 "webroot/apps/ScreenTree/ScreenTreeNested" 1033 ]
977 ] 1034 ]
978 1035 }
979 // Add component-specific screens 1036
980 def componentScreenLocs = ec.entity.find("moqui.screen.SubscreensItem") 1037 // Use discovered screens instead of hardcoded list
981 .selectFields(["screenLocation", "subscreenLocation"]) 1038 for (screenPath in accessibleScreens) {
982 .disableAuthz() 1039 try {
983 .distinct(true) 1040 // Skip screen paths that are obviously not main screens
984 .list() 1041 if (screenPath.contains("/subscreen/") || screenPath.contains("/popup/") ||
985 1042 screenPath.contains("/dialog/") || screenPath.contains("/error/")) {
986 for (compScreen in componentScreenLocs) { 1043 continue
987 if (compScreen.subscreenLocation) {
988 screenPaths << compScreen.subscreenLocation
989 } 1044 }
990 } 1045
991 1046 // Get screen definition to extract more information
992 // Combine all screen paths 1047 def screenDefinition = null
993 screenPaths.addAll(commonScreenPaths) 1048 try {
994 screenPaths = screenPaths.unique() 1049 screenDefinition = ec.screen.getScreenDefinition(screenPath)
995 1050 } catch (Exception e) {
996 // Filter by pattern if provided 1051 ec.logger.debug("Could not get screen definition for ${screenPath}: ${e.message}")
997 if (screenPathPattern) { 1052 continue
998 def pattern = screenPathPattern.replace("*", ".*") 1053 }
999 screenPaths = screenPaths.findAll { it.matches(pattern) } 1054
1000 } 1055 if (!screenDefinition) {
1001 1056 ec.logger.debug("No screen definition found for ${screenPath}")
1002 ec.logger.info("MCP Screen Discovery: Checking ${screenPaths.size()} screen paths") 1057 continue
1003 1058 }
1004 for (screenPath in screenPaths) { 1059
1060 // Extract screen information
1061 def title = screenDefinition.screenNode?.first("description")?.text ?: screenPath.split("/")[-1]
1062 def description = "Moqui screen: ${screenPath}"
1063 if (screenDefinition.screenNode?.first("description")?.text) {
1064 description = screenDefinition.screenNode.first("description").text
1065 }
1066
1067 // Get screen parameters from transitions
1068 def parameters = [:]
1005 try { 1069 try {
1006 // Check if user has permission to this screen 1070 def screenInfo = ec.screen.getScreenInfo(screenPath)
1007 if (userHasScreenPermission(screenPath)) { 1071 if (screenInfo?.transitionInfoByName) {
1008 def tool = convertScreenToMcpTool(screenPath, ec) 1072 for (transitionEntry in screenInfo.transitionInfoByName) {
1009 if (tool) { 1073 def transitionInfo = transitionEntry.value
1010 tools << tool 1074 // Add path parameters
1075 transitionInfo.ti?.getPathParameterList()?.each { param ->
1076 parameters[param] = [
1077 type: "string",
1078 description: "Path parameter for transition: ${param}"
1079 ]
1080 }
1081 // Add request parameters
1082 transitionInfo.ti?.getRequestParameterList()?.each { param ->
1083 parameters[param.name] = [
1084 type: "string",
1085 description: "Request parameter: ${param.name}"
1086 ]
1087 }
1011 } 1088 }
1012 } 1089 }
1013 } catch (Exception e) { 1090 } catch (Exception e) {
1014 ec.logger.debug("Error processing screen ${screenPath}: ${e.message}") 1091 ec.logger.debug("Could not extract parameters from screen ${screenPath}: ${e.message}")
1015 } 1092 }
1016 } 1093
1017 1094 ec.logger.info("MCP Screen Discovery: Adding accessible screen ${screenPath}")
1018 } finally { 1095 tools << createScreenTool(screenPath, title, description, parameters)
1019 if (adminUserInfo != null) { 1096
1020 ec.user.popUser() 1097 } catch (Exception e) {
1098 ec.logger.warn("Error processing screen ${screenPath}: ${e.message}")
1021 } 1099 }
1022 } 1100 }
1023 1101
1024 ec.logger.info("MCP Screen Discovery: Converted ${tools.size()} screens to MCP tools for user ${originalUsername}") 1102 ec.logger.info("MCP Screen Discovery: Created ${tools.size()} hardcoded screen tools for user ${originalUsername}")
1103
1104 result.tools = tools
1025 1105
1026 ]]></script> 1106 ]]></script>
1027 </actions> 1107 </actions>
1028 </service> 1108 </service>
1029 1109
1110
1111
1030 <service verb="convert" noun="ScreenToMcpTool" authenticate="false"> 1112 <service verb="convert" noun="ScreenToMcpTool" authenticate="false">
1031 <description>Convert a screen path to MCP tool format</description> 1113 <description>Convert a screen path to MCP tool format</description>
1032 <in-parameters> 1114 <in-parameters>
...@@ -1041,13 +1123,18 @@ ...@@ -1041,13 +1123,18 @@
1041 1123
1042 ExecutionContext ec = context.ec 1124 ExecutionContext ec = context.ec
1043 1125
1126 ec.logger.info("=== SCREEN TO MCP TOOL: ${screenPath} ===")
1127
1044 tool = null 1128 tool = null
1045 try { 1129 try {
1046 // Try to get screen definition 1130 // Try to get screen definition
1047 def screenDef = null 1131 def screenDef = null
1048 try { 1132 try {
1133 ec.logger.info("SCREEN TO MCP: Getting screen definition for ${screenPath}")
1049 screenDef = ec.screen.getScreenDefinition(screenPath) 1134 screenDef = ec.screen.getScreenDefinition(screenPath)
1135 ec.logger.info("SCREEN TO MCP: Got screen definition: ${screenDef ? 'YES' : 'NO'}")
1050 } catch (Exception e) { 1136 } catch (Exception e) {
1137 ec.logger.warn("SCREEN TO MCP: Error getting screen definition: ${e.message}")
1051 // Screen might not exist or be accessible 1138 // Screen might not exist or be accessible
1052 return 1139 return
1053 } 1140 }
...@@ -1143,6 +1230,22 @@ ...@@ -1143,6 +1230,22 @@
1143 tool.screenPath = screenPath 1230 tool.screenPath = screenPath
1144 tool.toolType = "screen" 1231 tool.toolType = "screen"
1145 1232
1233 // Add screen structure metadata
1234 try {
1235 def screenInfo = ec.screen.getScreenInfo(screenPath)
1236 if (screenInfo) {
1237 tool.screenInfo = [
1238 name: screenInfo.name,
1239 level: screenInfo.level,
1240 hasTransitions: screenInfo.transitions > 0,
1241 hasForms: screenInfo.forms > 0,
1242 subscreens: screenInfo.subscreens
1243 ]
1244 }
1245 } catch (Exception e) {
1246 ec.logger.debug("Could not get screen info for metadata: ${e.message}")
1247 }
1248
1146 } catch (Exception e) { 1249 } catch (Exception e) {
1147 ec.logger.warn("Error converting screen ${screenPath} to MCP tool: ${e.message}") 1250 ec.logger.warn("Error converting screen ${screenPath} to MCP tool: ${e.message}")
1148 } 1251 }
...@@ -1169,45 +1272,65 @@ ...@@ -1169,45 +1272,65 @@
1169 1272
1170 def startTime = System.currentTimeMillis() 1273 def startTime = System.currentTimeMillis()
1171 try { 1274 try {
1172 // Validate screen exists 1275 // Note: Screen validation will happen during render
1173 if (!ec.screen.isScreenDefined(screenPath)) { 1276 // if (!ec.screen.isScreenDefined(screenPath)) {
1174 throw new Exception("Screen not found: ${screenPath}") 1277 // throw new Exception("Screen not found: ${screenPath}")
1175 } 1278 // }
1176 1279
1177 // Set parameters in context 1280 // Set parameters in context
1178 if (parameters) { 1281 if (parameters) {
1179 ec.context.putAll(parameters) 1282 ec.context.putAll(parameters)
1180 } 1283 }
1181 1284
1182 // Render screen 1285 // Try to render screen content for LLM consumption
1183 def screenRender = ec.screen.makeRender() 1286 def output = null
1184 .screenPath(screenPath) 1287 def screenUrl = "http://localhost:8080/${screenPath}"
1185 .renderMode(renderMode) 1288
1289 try {
1290 ec.logger.info("MCP Screen Execution: Attempting to render screen ${screenPath}")
1291
1292 // Try to render screen as text for LLM to read
1293 def screenRender = ec.screen.makeRender()
1294 .rootScreen(screenPath) // Set root screen location
1295 .renderMode("text") // Set render mode, but don't set additional path for root screen
1296
1297 ec.logger.info("MCP Screen Execution: ScreenRender object created: ${screenRender?.getClass()?.getSimpleName()}")
1298
1299 if (screenRender) {
1300 output = screenRender.render()
1301 ec.logger.info("MCP Screen Execution: Successfully rendered screen ${screenPath}, output length: ${output?.length() ?: 0}")
1302 } else {
1303 throw new Exception("ScreenRender object is null")
1304 }
1305
1306 } catch (Exception e) {
1307 ec.logger.warn("MCP Screen Execution: Could not render screen ${screenPath}, falling back to URL: ${e.message}")
1308 ec.logger.warn("MCP Screen Execution: Exception details: ${e.getClass()?.getSimpleName()}: ${e.getMessage()}")
1309
1310 // Fallback to URL if rendering fails
1311 output = "Screen '${screenPath}' is accessible at: ${screenUrl}\n\nNote: Screen content could not be rendered. You can visit this URL in a web browser to interact with the screen directly."
1312 }
1186 1313
1187 def output = screenRender.render()
1188 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0 1314 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
1189 1315
1190 // Convert to MCP format 1316 def content = [
1191 def content = [] 1317 [
1192 if (output && output.trim().length() > 0) {
1193 content << [
1194 type: "text", 1318 type: "text",
1195 text: output 1319 text: output
1196 ] 1320 ]
1197 } 1321 ]
1198 1322
1199 result = [ 1323 result = [
1200 content: content, 1324 content: content,
1201 isError: false, 1325 isError: false,
1202 metadata: [ 1326 metadata: [
1203 screenPath: screenPath, 1327 screenPath: screenPath,
1204 renderMode: renderMode, 1328 screenUrl: screenUrl,
1205 executionTime: executionTime, 1329 executionTime: executionTime
1206 outputLength: output?.length() ?: 0
1207 ] 1330 ]
1208 ] 1331 ]
1209 1332
1210 ec.logger.info("MCP Screen Execution: Successfully executed screen ${screenPath} in ${executionTime}s") 1333 ec.logger.info("MCP Screen Execution: Generated URL for screen ${screenPath} in ${executionTime}s")
1211 1334
1212 } catch (Exception e) { 1335 } catch (Exception e) {
1213 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0 1336 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
......