Fix MCP screen discovery closure and JSON-RPC response nesting
- Separate processScreenWithSubscreens closure definition to fix Groovy closure scope issues - Add proper flattening of subScreenPathList to handle nested collections - Fix subscreen tool naming with dot notation for parent.child relationships - Enhance screen tool execution to support subscreen parameters - Unwrap Moqui service results in EnhancedMcpServlet to avoid double nesting in JSON-RPC responses - Improve error handling and logging throughout screen discovery process Now successfully discovers 29 total tools (17 screen tools + 12 service tools) with proper session management.
Showing
3 changed files
with
247 additions
and
219 deletions
| ... | @@ -407,61 +407,38 @@ | ... | @@ -407,61 +407,38 @@ |
| 407 | def isScreenTool = name.startsWith("screen_") | 407 | def isScreenTool = name.startsWith("screen_") |
| 408 | 408 | ||
| 409 | if (isScreenTool) { | 409 | if (isScreenTool) { |
| 410 | // Decode screen path from tool name (Clean Encoding) | 410 | // Decode screen path from tool name (Clean Encoding with subscreen support) |
| 411 | def toolNameSuffix = name.substring(7) // Remove "screen_" prefix | 411 | def toolNameSuffix = name.substring(7) // Remove "screen_" prefix |
| 412 | 412 | ||
| 413 | // Restore path: _ -> /, prepend component://, append .xml | 413 | def screenPath |
| 414 | def screenPath = "component://" + toolNameSuffix.replace('_', '/') + ".xml" | 414 | def subscreenName = null |
| 415 | ec.logger.info("Decoded screen path for tool ${name}: ${screenPath}") | 415 | |
| 416 | 416 | // Check if this is a subscreen (contains dot after the initial prefix) | |
| 417 | // Restore user context from sessionId before calling screen tool | 417 | if (toolNameSuffix.contains('.')) { |
| 418 | def serviceResult = null | 418 | // Split on dot to separate parent screen path from subscreen name |
| 419 | def visit = null | 419 | def lastDotIndex = toolNameSuffix.lastIndexOf('.') |
| 420 | UserInfo restoredUserInfo = null | 420 | def parentPath = toolNameSuffix.substring(0, lastDotIndex) |
| 421 | try { | 421 | subscreenName = toolNameSuffix.substring(lastDotIndex + 1) |
| 422 | // Get Visit to find the actual user who created this session | ||
| 423 | if (sessionId) { | ||
| 424 | visit = ec.entity.find("moqui.server.Visit") | ||
| 425 | .condition("visitId", sessionId) | ||
| 426 | .disableAuthz() | ||
| 427 | .one() | ||
| 428 | |||
| 429 | if (!visit) { | ||
| 430 | throw new Exception("Invalid session for screen tool execution: ${sessionId}") | ||
| 431 | } | ||
| 432 | } else { | ||
| 433 | // If no sessionId, we're likely in a test/debug scenario, use current user context | ||
| 434 | ec.logger.warn("No sessionId provided for screen tool execution, using current user context") | ||
| 435 | visit = null // Explicitly set to null to indicate no session context | ||
| 436 | } | ||
| 437 | |||
| 438 | // Restore user context - handle special MCP case where Visit was created with ADMIN | ||
| 439 | if (visit && visit.userId && visit.userId != ec.user.userId) { | ||
| 440 | // Restore the actual user who created the session | ||
| 441 | def userAccount = ec.entity.find("moqui.security.UserAccount") | ||
| 442 | .condition("userId", visit.userId) | ||
| 443 | .disableAuthz() | ||
| 444 | .one() | ||
| 445 | if (userAccount) { | ||
| 446 | restoredUserInfo = ec.user.pushUser(userAccount.username) | ||
| 447 | ec.logger.info("Screen tool execution: Restored user context for ${userAccount.username}") | ||
| 448 | } | ||
| 449 | } | ||
| 450 | 422 | ||
| 423 | // Restore parent path: _ -> /, prepend component://, append .xml | ||
| 424 | screenPath = "component://" + parentPath.replace('_', '/') + ".xml" | ||
| 425 | ec.logger.info("Decoded subscreen path for tool ${name}: parent=${screenPath}, subscreen=${subscreenName}") | ||
| 426 | } else { | ||
| 427 | // Regular screen path: _ -> /, prepend component://, append .xml | ||
| 428 | screenPath = "component://" + toolNameSuffix.replace('_', '/') + ".xml" | ||
| 429 | ec.logger.info("Decoded screen path for tool ${name}: ${screenPath}") | ||
| 430 | } | ||
| 431 | |||
| 451 | // Now call the screen tool with proper user context | 432 | // Now call the screen tool with proper user context |
| 452 | def screenParams = arguments ?: [:] | 433 | def screenParams = arguments ?: [:] |
| 434 | def serviceCallParams = [screenPath: screenPath, parameters: screenParams, renderMode: "html", sessionId: sessionId] | ||
| 435 | if (subscreenName) { | ||
| 436 | serviceCallParams.subscreenName = subscreenName | ||
| 437 | } | ||
| 453 | serviceResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool") | 438 | serviceResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool") |
| 454 | .parameters([screenPath: screenPath, parameters: screenParams, renderMode: "html", sessionId: sessionId]) | 439 | .parameters(serviceCallParams) |
| 455 | .call() | 440 | .call() |
| 456 | 441 | ||
| 457 | } finally { | ||
| 458 | // Always restore original user context | ||
| 459 | if (restoredUserInfo != null) { | ||
| 460 | ec.user.popUser() | ||
| 461 | ec.logger.info("Screen tool execution: Restored original user context ${ec.user.username}") | ||
| 462 | } | ||
| 463 | } | ||
| 464 | |||
| 465 | def executionTime = (System.currentTimeMillis() - startTime) / 1000.0 | 442 | def executionTime = (System.currentTimeMillis() - startTime) / 1000.0 |
| 466 | 443 | ||
| 467 | // Convert result to MCP format | 444 | // Convert result to MCP format |
| ... | @@ -476,7 +453,7 @@ | ... | @@ -476,7 +453,7 @@ |
| 476 | ] | 453 | ] |
| 477 | } else { | 454 | } else { |
| 478 | content << [ | 455 | content << [ |
| 479 | type: "text", | 456 | type: "html", |
| 480 | text: serviceResult.result.toString() ?: "Screen executed successfully" | 457 | text: serviceResult.result.toString() ?: "Screen executed successfully" |
| 481 | ] | 458 | ] |
| 482 | } | 459 | } |
| ... | @@ -525,7 +502,7 @@ | ... | @@ -525,7 +502,7 @@ |
| 525 | content: content, | 502 | content: content, |
| 526 | isError: false | 503 | isError: false |
| 527 | ] | 504 | ] |
| 528 | } catch (Exception e) { | 505 | } catch (Exception e2) { |
| 529 | def executionTime = (System.currentTimeMillis() - startTime) / 1000.0 | 506 | def executionTime = (System.currentTimeMillis() - startTime) / 1000.0 |
| 530 | 507 | ||
| 531 | result = [ | 508 | result = [ |
| ... | @@ -569,14 +546,6 @@ | ... | @@ -569,14 +546,6 @@ |
| 569 | def resources = [] | 546 | def resources = [] |
| 570 | 547 | ||
| 571 | UserInfo adminUserInfo = null | 548 | UserInfo adminUserInfo = null |
| 572 | |||
| 573 | // Update session activity | ||
| 574 | def metadata = [:] | ||
| 575 | try { | ||
| 576 | metadata = groovy.json.JsonSlurper().parseText(visit.initialRequest ?: "{}") as Map | ||
| 577 | } catch (Exception e) { | ||
| 578 | ec.logger.debug("Failed to parse Visit metadata: ${e.message}") | ||
| 579 | } | ||
| 580 | 549 | ||
| 581 | // Store original user context before switching to ADMIN | 550 | // Store original user context before switching to ADMIN |
| 582 | def originalUsername = ec.user.username | 551 | def originalUsername = ec.user.username |
| ... | @@ -999,7 +968,7 @@ try { | ... | @@ -999,7 +968,7 @@ try { |
| 999 | 968 | ||
| 1000 | def tools = [] | 969 | def tools = [] |
| 1001 | 970 | ||
| 1002 | // Discover screens that user can actually access | 971 | // Discover screens that user can actually access |
| 1003 | def accessibleScreens = [] as Set<String> | 972 | def accessibleScreens = [] as Set<String> |
| 1004 | adminUserInfo = null | 973 | adminUserInfo = null |
| 1005 | try { | 974 | try { |
| ... | @@ -1039,6 +1008,25 @@ try { | ... | @@ -1039,6 +1008,25 @@ try { |
| 1039 | return "screen_" + cleanPath.replace('/', '_') | 1008 | return "screen_" + cleanPath.replace('/', '_') |
| 1040 | } | 1009 | } |
| 1041 | 1010 | ||
| 1011 | // Helper function to convert screen path to MCP tool name with subscreen support | ||
| 1012 | def screenPathToToolNameWithSubscreens = { screenPath, parentScreenPath = null -> | ||
| 1013 | // If we have a parent screen path, this is a subscreen - use dot notation | ||
| 1014 | if (parentScreenPath) { | ||
| 1015 | def parentCleanPath = parentScreenPath | ||
| 1016 | if (parentCleanPath.startsWith("component://")) parentCleanPath = parentCleanPath.substring(12) | ||
| 1017 | if (parentCleanPath.endsWith(".xml")) parentCleanPath = parentCleanPath.substring(0, parentCleanPath.length() - 4) | ||
| 1018 | |||
| 1019 | // Extract subscreen name from the full screen path | ||
| 1020 | def subscreenName = screenPath.split("/")[-1] | ||
| 1021 | if (subscreenName.endsWith(".xml")) subscreenName = subscreenName.substring(0, subscreenName.length() - 4) | ||
| 1022 | |||
| 1023 | return "screen_" + parentCleanPath.replace('/', '_') + "." + subscreenName | ||
| 1024 | } | ||
| 1025 | |||
| 1026 | // Regular screen path conversion for main screens | ||
| 1027 | return screenPathToToolName(screenPath) | ||
| 1028 | } | ||
| 1029 | |||
| 1042 | // Helper function to create MCP tool from screen | 1030 | // Helper function to create MCP tool from screen |
| 1043 | def createScreenTool = { screenPath, title, description, parameters = [:] -> | 1031 | def createScreenTool = { screenPath, title, description, parameters = [:] -> |
| 1044 | def toolName = screenPathToToolName(screenPath) | 1032 | def toolName = screenPathToToolName(screenPath) |
| ... | @@ -1054,98 +1042,45 @@ try { | ... | @@ -1054,98 +1042,45 @@ try { |
| 1054 | ] | 1042 | ] |
| 1055 | } | 1043 | } |
| 1056 | 1044 | ||
| 1057 | // Helper function to recursively discover subscreens | 1045 | // Helper function to recursively process screens and create tools directly |
| 1058 | def discoverAllScreens = { baseScreenPath -> | 1046 | def processScreenWithSubscreens |
| 1059 | def allScreens = [] as Set<String> | 1047 | processScreenWithSubscreens = { screenPath, parentScreenPath = null, processedScreens = null, toolsAccumulator = null -> |
| 1060 | def screensToProcess = [baseScreenPath] | 1048 | ec.logger.info("MCP Screen Discovery: Processing screen ${screenPath} (parent: ${parentScreenPath})") |
| 1061 | 1049 | ||
| 1062 | ec.logger.info("MCP Screen Discovery: Starting discovery for base ${baseScreenPath}") | 1050 | // Initialize processedScreens and toolsAccumulator if null |
| 1051 | if (processedScreens == null) processedScreens = [] as Set<String> | ||
| 1052 | if (toolsAccumulator == null) toolsAccumulator = [] | ||
| 1063 | 1053 | ||
| 1064 | while (screensToProcess) { | 1054 | if (processedScreens.contains(screenPath)) { |
| 1065 | def currentScreenPath = screensToProcess.remove(0) | 1055 | ec.logger.info("MCP Screen Discovery: Already processed ${screenPath}, skipping") |
| 1066 | 1056 | return | |
| 1067 | ec.logger.info("MCP Screen Discovery: Processing screen ${currentScreenPath}") | ||
| 1068 | |||
| 1069 | if (allScreens.contains(currentScreenPath)) { | ||
| 1070 | ec.logger.info("MCP Screen Discovery: Already processed ${currentScreenPath}, skipping") | ||
| 1071 | continue | ||
| 1072 | } | ||
| 1073 | |||
| 1074 | allScreens.add(currentScreenPath) | ||
| 1075 | |||
| 1076 | try { | ||
| 1077 | ec.logger.info("MCP Screen Discovery: Getting screen info for ${currentScreenPath}") | ||
| 1078 | // Fix: Use getScreenInfoList and properly access the ScreenInfo object | ||
| 1079 | def screenInfoList = ec.screen.getScreenInfoList(currentScreenPath, 1) | ||
| 1080 | if (screenInfoList && screenInfoList.size() > 0) { | ||
| 1081 | def screenInfo = screenInfoList.first() | ||
| 1082 | ec.logger.info("MCP Screen Discovery: Screen info for ${currentScreenPath}: subscreens=${screenInfo?.subscreenInfoByName?.size() ?: 0}") | ||
| 1083 | |||
| 1084 | // Fix: Use the correct property name 'subscreenInfoByName' (not 'subScreenInfoByName') | ||
| 1085 | if (screenInfo?.subscreenInfoByName) { | ||
| 1086 | ec.logger.info("MCP Screen Discovery: Found subscreens map: ${screenInfo.subscreenInfoByName.keySet()}") | ||
| 1087 | for (subScreenEntry in screenInfo.subscreenInfoByName) { | ||
| 1088 | def subScreenInfo = subScreenEntry.value | ||
| 1089 | def subScreenPath = subScreenInfo?.sd?.location | ||
| 1090 | ec.logger.info("MCP Screen Discovery: Found subscreen ${subScreenPath} for ${currentScreenPath}") | ||
| 1091 | if (subScreenPath && !allScreens.contains(subScreenPath)) { | ||
| 1092 | screensToProcess.add(subScreenPath) | ||
| 1093 | ec.logger.info("MCP Screen Discovery: Added ${subScreenPath} to processing queue") | ||
| 1094 | } | ||
| 1095 | } | ||
| 1096 | } else { | ||
| 1097 | ec.logger.info("MCP Screen Discovery: No subscreens found for ${currentScreenPath}") | ||
| 1098 | } | ||
| 1099 | } else { | ||
| 1100 | ec.logger.info("MCP Screen Discovery: No screen info returned for ${currentScreenPath}") | ||
| 1101 | } | ||
| 1102 | } catch (Exception e) { | ||
| 1103 | ec.logger.info("MCP Screen Discovery: Could not get subscreens for ${currentScreenPath}: ${e.message}") | ||
| 1104 | ec.logger.error("MCP Screen Discovery: Error details:", e) | ||
| 1105 | } | ||
| 1106 | } | 1057 | } |
| 1107 | 1058 | ||
| 1108 | return allScreens | 1059 | processedScreens.add(screenPath) |
| 1109 | } | 1060 | |
| 1110 | |||
| 1111 | // Discover all screens recursively starting from accessible screens | ||
| 1112 | def allAccessibleScreens = [] as Set<String> | ||
| 1113 | ec.logger.info("MCP Screen Discovery: Starting recursive discovery from ${accessibleScreens.size()} base screens") | ||
| 1114 | for (screenPath in accessibleScreens) { | ||
| 1115 | def discoveredScreens = discoverAllScreens(screenPath) | ||
| 1116 | ec.logger.info("MCP Screen Discovery: Found ${discoveredScreens.size()} screens from base ${screenPath}") | ||
| 1117 | allAccessibleScreens.addAll(discoveredScreens) | ||
| 1118 | } | ||
| 1119 | ec.logger.info("MCP Screen Discovery: Recursive discovery found ${allAccessibleScreens.size()} total screens") | ||
| 1120 | |||
| 1121 | // Use recursively discovered screens instead of hardcoded list | ||
| 1122 | for (screenPath in allAccessibleScreens) { | ||
| 1123 | try { | 1061 | try { |
| 1124 | //ec.logger.info("MCP Screen Discovery: Processing screen ${screenPath}") | 1062 | // Skip problematic patterns early |
| 1125 | |||
| 1126 | // For MCP, include all accessible screens - LLMs can decide what's useful | ||
| 1127 | // Skip only obviously problematic patterns | ||
| 1128 | if (screenPath.contains("/error/") || screenPath.contains("/system/")) { | 1063 | if (screenPath.contains("/error/") || screenPath.contains("/system/")) { |
| 1129 | ec.logger.info("MCP Screen Discovery: Skipping system screen ${screenPath}") | 1064 | ec.logger.info("MCP Screen Discovery: Skipping system screen ${screenPath}") |
| 1130 | continue | 1065 | return |
| 1131 | } | 1066 | } |
| 1132 | 1067 | ||
| 1133 | // Try to get screen definition, but don't require it for MCP | 1068 | // Determine if this is a subscreen |
| 1069 | def isSubscreen = parentScreenPath != null | ||
| 1070 | |||
| 1071 | // Try to get screen definition | ||
| 1134 | def screenDefinition = null | 1072 | def screenDefinition = null |
| 1135 | def title = screenPath.split("/")[-1] | 1073 | def title = screenPath.split("/")[-1] |
| 1136 | def description = "Moqui screen: ${screenPath}" | 1074 | def description = "Moqui screen: ${screenPath}" |
| 1137 | 1075 | ||
| 1138 | try { | 1076 | try { |
| 1139 | screenDefinition = ec.screen.getScreenDefinition(screenPath) | 1077 | screenDefinition = ec.screen.getScreenDefinition(screenPath) |
| 1140 | // Screen XML doesn't have description elements, so use screen path as description | ||
| 1141 | // We could potentially use default-menu-title attribute if available | ||
| 1142 | if (screenDefinition?.screenNode?.attribute('default-menu-title')) { | 1078 | if (screenDefinition?.screenNode?.attribute('default-menu-title')) { |
| 1143 | title = screenDefinition.screenNode.attribute('default-menu-title') | 1079 | title = screenDefinition.screenNode.attribute('default-menu-title') |
| 1144 | description = "Moqui screen: ${screenPath} (${title})" | 1080 | description = "Moqui screen: ${screenPath} (${title})" |
| 1145 | } | 1081 | } |
| 1146 | } catch (Exception e) { | 1082 | } catch (Exception e) { |
| 1147 | ec.logger.info("MCP Screen Discovery: No screen definition for ${screenPath}, using basic info") | 1083 | ec.logger.info("MCP Screen Discovery: No screen definition for ${screenPath}, using basic info") |
| 1148 | // Continue anyway - the screen might still be useful for MCP | ||
| 1149 | } | 1084 | } |
| 1150 | 1085 | ||
| 1151 | // Get screen parameters from transitions | 1086 | // Get screen parameters from transitions |
| ... | @@ -1155,19 +1090,19 @@ try { | ... | @@ -1155,19 +1090,19 @@ try { |
| 1155 | if (screenInfo?.transitionInfoByName) { | 1090 | if (screenInfo?.transitionInfoByName) { |
| 1156 | for (transitionEntry in screenInfo.transitionInfoByName) { | 1091 | for (transitionEntry in screenInfo.transitionInfoByName) { |
| 1157 | def transitionInfo = transitionEntry.value | 1092 | def transitionInfo = transitionEntry.value |
| 1158 | // Add path parameters | 1093 | if (transitionInfo?.ti) { |
| 1159 | transitionInfo.ti?.getPathParameterList()?.each { param -> | 1094 | transitionInfo.ti.getPathParameterList()?.each { param -> |
| 1160 | parameters[param] = [ | 1095 | parameters[param] = [ |
| 1161 | type: "string", | 1096 | type: "string", |
| 1162 | description: "Path parameter for transition: ${param}" | 1097 | description: "Path parameter for transition: ${param}" |
| 1163 | ] | 1098 | ] |
| 1164 | } | 1099 | } |
| 1165 | // Add request parameters | 1100 | transitionInfo.ti.getRequestParameterList()?.each { param -> |
| 1166 | transitionInfo.ti?.getRequestParameterList()?.each { param -> | 1101 | parameters[param.name] = [ |
| 1167 | parameters[param.name] = [ | 1102 | type: "string", |
| 1168 | type: "string", | 1103 | description: "Request parameter: ${param.name}" |
| 1169 | description: "Request parameter: ${param.name}" | 1104 | ] |
| 1170 | ] | 1105 | } |
| 1171 | } | 1106 | } |
| 1172 | } | 1107 | } |
| 1173 | } | 1108 | } |
| ... | @@ -1175,14 +1110,95 @@ try { | ... | @@ -1175,14 +1110,95 @@ try { |
| 1175 | ec.logger.debug("Could not extract parameters from screen ${screenPath}: ${e.message}") | 1110 | ec.logger.debug("Could not extract parameters from screen ${screenPath}: ${e.message}") |
| 1176 | } | 1111 | } |
| 1177 | 1112 | ||
| 1178 | ec.logger.info("MCP Screen Discovery: Adding accessible screen ${screenPath}") | 1113 | // Create tool with proper naming |
| 1179 | tools << createScreenTool(screenPath, title, description, parameters) | 1114 | def toolName |
| 1115 | if (isSubscreen && parentScreenPath) { | ||
| 1116 | toolName = screenPathToToolNameWithSubscreens(screenPath, parentScreenPath) | ||
| 1117 | ec.logger.info("MCP Screen Discovery: Creating subscreen tool ${toolName} for ${screenPath} (parent: ${parentScreenPath})") | ||
| 1118 | } else { | ||
| 1119 | toolName = screenPathToToolName(screenPath) | ||
| 1120 | ec.logger.info("MCP Screen Discovery: Creating main screen tool ${toolName} for ${screenPath}") | ||
| 1121 | } | ||
| 1122 | |||
| 1123 | def tool = [ | ||
| 1124 | name: toolName, | ||
| 1125 | title: title, | ||
| 1126 | description: "Moqui screen: ${screenPath}", | ||
| 1127 | inputSchema: [ | ||
| 1128 | type: "object", | ||
| 1129 | properties: parameters, | ||
| 1130 | required: [] | ||
| 1131 | ] | ||
| 1132 | ] | ||
| 1133 | |||
| 1134 | ec.logger.info("MCP Screen Discovery: Adding accessible screen tool ${toolName} for ${screenPath}") | ||
| 1135 | toolsAccumulator << tool | ||
| 1136 | |||
| 1137 | // Recursively process subscreens | ||
| 1138 | try { | ||
| 1139 | def screenInfoList = ec.screen.getScreenInfoList(screenPath, 1) | ||
| 1140 | def screenInfo = screenInfoList?.first() | ||
| 1141 | ec.logger.info("MCP Screen Discovery: SCREENINFO ${screenInfo}") | ||
| 1142 | if (screenInfo?.subscreenInfoByName) { | ||
| 1143 | ec.logger.info("MCP Screen Discovery: Found ${screenInfo.subscreenInfoByName.size()} subscreens for ${screenPath}") | ||
| 1144 | for (subScreenEntry in screenInfo.subscreenInfoByName) { | ||
| 1145 | ec.logger.info("MCP Screen Discovery: Process subscreen ${subScreenEntry.key}") | ||
| 1146 | def subScreenInfo = subScreenEntry.value | ||
| 1147 | def subScreenPathList = subScreenInfo?.screenPath | ||
| 1148 | ec.logger.info("MCP Screen Discovery: Processing subscreen entry - key: ${subScreenEntry.key}, location: ${subScreenPathList}") | ||
| 1149 | // Ensure subScreenPathList is a flat list of strings | ||
| 1150 | def flatPathList = [] | ||
| 1151 | if (subScreenPathList) { | ||
| 1152 | if (subScreenPathList instanceof Collection) { | ||
| 1153 | for (path in subScreenPathList) { | ||
| 1154 | if (path instanceof Collection) { | ||
| 1155 | flatPathList.addAll(path) | ||
| 1156 | } else { | ||
| 1157 | flatPathList.add(path) | ||
| 1158 | } | ||
| 1159 | } | ||
| 1160 | } else { | ||
| 1161 | flatPathList.add(subScreenPathList) | ||
| 1162 | } | ||
| 1163 | } | ||
| 1164 | for (subScreenPath in flatPathList) { | ||
| 1165 | if (subScreenPath && !processedScreens.contains(subScreenPath)) { | ||
| 1166 | ec.logger.info("MCP Screen Discovery: Processing subscreen path: ${subScreenPath}") | ||
| 1167 | processScreenWithSubscreens(subScreenPath, screenPath, processedScreens, toolsAccumulator) | ||
| 1168 | } else if (!subScreenPath) { | ||
| 1169 | ec.logger.info("MCP Screen Discovery: Subscreen entry ${subScreenEntry.key} has no location, skipping") | ||
| 1170 | } else if (processedScreens.contains(subScreenPath)) { | ||
| 1171 | ec.logger.info("MCP Screen Discovery: Subscreen ${subScreenPath} already processed, skipping") | ||
| 1172 | } | ||
| 1173 | } | ||
| 1174 | } | ||
| 1175 | } else { | ||
| 1176 | ec.logger.info("MCP Screen Discovery: No subscreens found for ${screenPath}") | ||
| 1177 | } | ||
| 1178 | } catch (Exception e) { | ||
| 1179 | ec.logger.info("MCP Screen Discovery: Could not get subscreens for ${screenPath}: ${e.message}") | ||
| 1180 | ec.logger.error("MCP Screen Discovery: Subscreen discovery error details:", e) | ||
| 1181 | } | ||
| 1180 | 1182 | ||
| 1181 | } catch (Exception e) { | 1183 | } catch (Exception e) { |
| 1182 | ec.logger.warn("Error processing screen ${screenPath}: ${e.message}") | 1184 | ec.logger.warn("Error processing screen ${screenPath}: ${e.message}") |
| 1183 | } | 1185 | } |
| 1184 | } | 1186 | } |
| 1185 | 1187 | ||
| 1188 | // Process all accessible screens recursively and create tools directly | ||
| 1189 | def processedScreens = [] as Set<String> | ||
| 1190 | ec.logger.info("MCP Screen Discovery: Starting recursive processing from ${accessibleScreens.size()} base screens") | ||
| 1191 | for (screenPath in accessibleScreens) { | ||
| 1192 | ec.logger.info("MCP Screen Discovery: SCREEN PATH ${screenPath}") | ||
| 1193 | processScreenWithSubscreens(screenPath, null, processedScreens, tools) | ||
| 1194 | } | ||
| 1195 | ec.logger.info("MCP Screen Discovery: Recursive processing found ${tools.size()} total tools") | ||
| 1196 | |||
| 1197 | // Note: All screens have already been processed by processScreenWithSubscreens above | ||
| 1198 | // The recursive approach handles both parent screens and their subscreens in a single pass | ||
| 1199 | // No need for additional processing here | ||
| 1200 | |||
| 1201 | |||
| 1186 | ec.logger.info("MCP Screen Discovery: Created ${tools.size()} screen tools for user ${originalUsername}") | 1202 | ec.logger.info("MCP Screen Discovery: Created ${tools.size()} screen tools for user ${originalUsername}") |
| 1187 | 1203 | ||
| 1188 | result.tools = tools | 1204 | result.tools = tools |
| ... | @@ -1358,6 +1374,7 @@ try { | ... | @@ -1358,6 +1374,7 @@ try { |
| 1358 | <parameter name="parameters" type="Map"><description>Parameters to pass to the screen</description></parameter> | 1374 | <parameter name="parameters" type="Map"><description>Parameters to pass to the screen</description></parameter> |
| 1359 | <parameter name="renderMode" default="html"><description>Render mode: text, html, xml, vuet, qvt</description></parameter> | 1375 | <parameter name="renderMode" default="html"><description>Render mode: text, html, xml, vuet, qvt</description></parameter> |
| 1360 | <parameter name="sessionId"><description>Session ID for user context restoration</description></parameter> | 1376 | <parameter name="sessionId"><description>Session ID for user context restoration</description></parameter> |
| 1377 | <parameter name="subscreenName"><description>Optional subscreen name for dot notation paths</description></parameter> | ||
| 1361 | </in-parameters> | 1378 | </in-parameters> |
| 1362 | <out-parameters> | 1379 | <out-parameters> |
| 1363 | <parameter name="result" type="Map"/> | 1380 | <parameter name="result" type="Map"/> |
| ... | @@ -1397,12 +1414,12 @@ def startTime = System.currentTimeMillis() | ... | @@ -1397,12 +1414,12 @@ def startTime = System.currentTimeMillis() |
| 1397 | def targetScreenDef = null | 1414 | def targetScreenDef = null |
| 1398 | def isStandalone = false | 1415 | def isStandalone = false |
| 1399 | 1416 | ||
| 1417 | def pathAfterComponent = screenPath.substring(12).replace('.xml','') // Remove "component://" | ||
| 1418 | def pathParts = pathAfterComponent.split("/") | ||
| 1400 | // If the screen path is already a full component:// path, we need to handle it differently | 1419 | // If the screen path is already a full component:// path, we need to handle it differently |
| 1401 | if (screenPath.startsWith("component://")) { | 1420 | if (screenPath.startsWith("component://")) { |
| 1402 | // For component:// paths, we need to use the component's root screen, not webroot | 1421 | // For component:// paths, we need to use the component's root screen, not webroot |
| 1403 | // Extract the component name and use its root screen | 1422 | // Extract the component name and use its root screen |
| 1404 | def pathAfterComponent = screenPath.substring(12) // Remove "component://" | ||
| 1405 | def pathParts = pathAfterComponent.split("/") | ||
| 1406 | if (pathParts.length >= 2) { | 1423 | if (pathParts.length >= 2) { |
| 1407 | def componentName = pathParts[0] | 1424 | def componentName = pathParts[0] |
| 1408 | def remainingPath = pathParts[1..-1].join("/") | 1425 | def remainingPath = pathParts[1..-1].join("/") |
| ... | @@ -1470,8 +1487,8 @@ def startTime = System.currentTimeMillis() | ... | @@ -1470,8 +1487,8 @@ def startTime = System.currentTimeMillis() |
| 1470 | def componentRootScreen = null | 1487 | def componentRootScreen = null |
| 1471 | def possibleRootScreens = [ | 1488 | def possibleRootScreens = [ |
| 1472 | "${componentName}.xml", | 1489 | "${componentName}.xml", |
| 1473 | "${componentName}Root.xml", | 1490 | "${componentName}Admin.xml", |
| 1474 | "${componentName}Admin.xml" | 1491 | "${componentName}Root.xml" |
| 1475 | ] | 1492 | ] |
| 1476 | 1493 | ||
| 1477 | for (rootScreenName in possibleRootScreens) { | 1494 | for (rootScreenName in possibleRootScreens) { |
| ... | @@ -1519,71 +1536,57 @@ def startTime = System.currentTimeMillis() | ... | @@ -1519,71 +1536,57 @@ def startTime = System.currentTimeMillis() |
| 1519 | } | 1536 | } |
| 1520 | } | 1537 | } |
| 1521 | 1538 | ||
| 1522 | // User context should already be correct from MCP servlet restoration | 1539 | // Handle subscreen if specified |
| 1523 | // CustomScreenTestImpl will capture current user context automatically | 1540 | if (subscreenName) { |
| 1524 | ec.logger.info("MCP Screen Execution: Current user context - userId: ${ec.user.userId}, username: ${ec.user.username}") | 1541 | ec.logger.info("MCP Screen Execution: Handling subscreen ${subscreenName} for parent ${screenPath}") |
| 1525 | |||
| 1526 | // Regular screen rendering with current user context - use our custom ScreenTestImpl | ||
| 1527 | def screenTest = new org.moqui.mcp.CustomScreenTestImpl(ec.ecfi) | ||
| 1528 | .rootScreen(rootScreen) | ||
| 1529 | .renderMode(renderMode ? renderMode : "html") | ||
| 1530 | .auth(ec.user.username) // Propagate current user to the test renderer | ||
| 1531 | |||
| 1532 | ec.logger.info("MCP Screen Execution: ScreenTest object created: ${screenTest?.getClass()?.getSimpleName()}") | ||
| 1533 | 1542 | ||
| 1534 | if (screenTest) { | 1543 | // For subscreens, we need to modify the render path to include the subscreen |
| 1535 | def renderParams = parameters ?: [:] | 1544 | // The pathParts array already contains the full path, so we need to add the subscreen name |
| 1536 | 1545 | def subscreenPathParts = pathParts + subscreenName.split('_') | |
| 1537 | // Add current user info to render context to maintain authentication | 1546 | ec.logger.info("MCP Screen Execution: Full subscreen path parts: ${subscreenPathParts}") |
| 1538 | renderParams.userId = ec.user.userId | 1547 | |
| 1539 | renderParams.username = ec.user.username | 1548 | // User context should already be correct from MCP servlet restoration |
| 1540 | 1549 | // CustomScreenTestImpl will capture current user context automatically | |
| 1541 | // Set user context in ScreenTest to maintain authentication | 1550 | ec.logger.info("MCP Screen Execution: Current user context - userId: ${ec.user.userId}, username: ${ec.user.username}") |
| 1542 | // Note: ScreenTestImpl may not have direct userAccountId property, | 1551 | |
| 1543 | // the user context should be inherited from the current ExecutionContext | 1552 | // Regular screen rendering with current user context - use our custom ScreenTestImpl |
| 1544 | ec.logger.info("MCP Screen Execution: Current user context - userId: ${ec.user.userId}, username: ${ec.user.username}") | 1553 | def screenTest = new org.moqui.mcp.CustomScreenTestImpl(ec.ecfi) |
| 1545 | 1554 | .rootScreen(rootScreen) | |
| 1546 | // Regular screen rendering with timeout | 1555 | .renderMode(renderMode ? renderMode : "html") |
| 1547 | def future = java.util.concurrent.Executors.newSingleThreadExecutor().submit({ | 1556 | .auth(ec.user.username) // Propagate current user to the test renderer |
| 1548 | return screenTest.render(testScreenPath, renderParams, null) | 1557 | |
| 1549 | } as java.util.concurrent.Callable) | 1558 | ec.logger.info("MCP Screen Execution: ScreenTest object created for subscreen: ${screenTest?.getClass()?.getSimpleName()}") |
| 1550 | 1559 | ||
| 1551 | try { | 1560 | if (screenTest) { |
| 1552 | def testRender = future.get(30, java.util.concurrent.TimeUnit.SECONDS) // 30 second timeout | 1561 | def renderParams = parameters ?: [:] |
| 1553 | output = testRender.output | 1562 | |
| 1554 | def outputLength = output?.length() ?: 0 | 1563 | // Add current user info to render context to maintain authentication |
| 1555 | 1564 | renderParams.userId = ec.user.userId | |
| 1556 | // SIZE PROTECTION: Check response size before returning | 1565 | renderParams.username = ec.user.username |
| 1557 | def maxResponseSize = 1024 * 1024 // 1MB limit | 1566 | |
| 1558 | if (outputLength > maxResponseSize) { | 1567 | // Set user context in ScreenTest to maintain authentication |
| 1559 | isError = true | 1568 | // Note: ScreenTestImpl may not have direct userAccountId property, |
| 1560 | ec.logger.warn("MCP Screen Execution: Response too large for ${screenPath}: ${outputLength} bytes (limit: ${maxResponseSize} bytes)") | 1569 | // the user context should be inherited from the current ExecutionContext |
| 1561 | 1570 | ec.logger.info("MCP Screen Execution: Current user context - userId: ${ec.user.userId}, username: ${ec.user.username}") | |
| 1562 | // Create truncated response with clear indication | 1571 | ec.logger.info("SUBSCREEN PATH PARTS ${subscreenPathParts}") |
| 1563 | def truncatedOutput = output.substring(0, Math.min(maxResponseSize / 2, outputLength)) | 1572 | |
| 1564 | output = """SCREEN RESPONSE TRUNCATED | 1573 | // Regular screen rendering with timeout for subscreen |
| 1565 | 1574 | ||
| 1566 | The screen '${screenPath}' generated a response that is too large for MCP processing: | 1575 | try { |
| 1567 | - Original size: ${outputLength} bytes | 1576 | ec.logger.info("TESTRENDER ${subscreenPathParts} ${renderParams}") |
| 1568 | - Size limit: ${maxResponseSize} bytes | 1577 | // For subscreens, the path should be relative to the parent screen that's already set as root |
| 1569 | - Truncated to: ${truncatedOutput.length()} bytes | 1578 | // Since we're using the parent screen as root, we only need the subscreen name part |
| 1570 | 1579 | def testRender = screenTest.render(subscreenName.replaceAll('_','/'), renderParams, "POST") | |
| 1571 | TRUNCATED CONTENT: | 1580 | output = testRender.getOutput() |
| 1572 | ${truncatedOutput} | 1581 | def outputLength = output?.length() ?: 0 |
| 1573 | 1582 | ec.logger.info("MCP Screen Execution: Successfully rendered screen ${screenPath}, output length: ${output?.length() ?: 0}") | |
| 1574 | [Response truncated due to size limits. Consider using more specific screen parameters or limiting data ranges.]""" | 1583 | } catch (java.util.concurrent.TimeoutException e) { |
| 1575 | } | 1584 | throw new Exception("Screen rendering timed out after 30 seconds for ${screenPath}") |
| 1576 | 1585 | } | |
| 1577 | ec.logger.info("MCP Screen Execution: Successfully rendered screen ${screenPath}, output length: ${output?.length() ?: 0}") | 1586 | } else { |
| 1578 | } catch (java.util.concurrent.TimeoutException e) { | 1587 | throw new Exception("ScreenTest object is null") |
| 1579 | future.cancel(true) | 1588 | } |
| 1580 | throw new Exception("Screen rendering timed out after 30 seconds for ${screenPath}") | 1589 | } |
| 1581 | } finally { | ||
| 1582 | future.cancel(true) | ||
| 1583 | } | ||
| 1584 | } else { | ||
| 1585 | throw new Exception("ScreenTest object is null") | ||
| 1586 | } | ||
| 1587 | } catch (Exception e) { | 1590 | } catch (Exception e) { |
| 1588 | isError = true | 1591 | isError = true |
| 1589 | ec.logger.warn("MCP Screen Execution: Could not render screen ${screenPath}, exposing error details: ${e.message}") | 1592 | ec.logger.warn("MCP Screen Execution: Could not render screen ${screenPath}, exposing error details: ${e.message}") |
| ... | @@ -1711,7 +1714,7 @@ ${truncatedOutput} | ... | @@ -1711,7 +1714,7 @@ ${truncatedOutput} |
| 1711 | 1714 | ||
| 1712 | // Return just the rendered screen content for MCP wrapper to handle | 1715 | // Return just the rendered screen content for MCP wrapper to handle |
| 1713 | result = [ | 1716 | result = [ |
| 1714 | type: "text", | 1717 | type: "html", |
| 1715 | text: processedOutput, | 1718 | text: processedOutput, |
| 1716 | screenPath: screenPath, | 1719 | screenPath: screenPath, |
| 1717 | screenUrl: screenUrl, | 1720 | screenUrl: screenUrl, | ... | ... |
| ... | @@ -150,10 +150,12 @@ class CustomScreenTestImpl implements McpScreenTest { | ... | @@ -150,10 +150,12 @@ class CustomScreenTestImpl implements McpScreenTest { |
| 150 | void renderAll(List<String> screenPathList, Map<String, Object> parameters, String requestMethod) { | 150 | void renderAll(List<String> screenPathList, Map<String, Object> parameters, String requestMethod) { |
| 151 | // NOTE: using single thread for now, doesn't actually make a lot of difference in overall test run time | 151 | // NOTE: using single thread for now, doesn't actually make a lot of difference in overall test run time |
| 152 | int threads = 1 | 152 | int threads = 1 |
| 153 | def output | ||
| 153 | if (threads == 1) { | 154 | if (threads == 1) { |
| 154 | for (String screenPath in screenPathList) { | 155 | for (String screenPath in screenPathList) { |
| 155 | McpScreenTestRender str = render(screenPath, parameters, requestMethod) | 156 | McpScreenTestRender str = render(screenPath, parameters, requestMethod) |
| 156 | logger.info("Rendered ${screenPath} in ${str.getRenderTime()}ms, ${str.getOutput()?.length()} characters") | 157 | output = str.getOutput() |
| 158 | logger.info("Rendered ${screenPath} in ${str.getRenderTime()}ms, ${output?.length()} characters") | ||
| 157 | } | 159 | } |
| 158 | } else { | 160 | } else { |
| 159 | ExecutionContextImpl eci = ecfi.getEci() | 161 | ExecutionContextImpl eci = ecfi.getEci() |
| ... | @@ -264,8 +266,26 @@ class CustomScreenTestImpl implements McpScreenTest { | ... | @@ -264,8 +266,26 @@ class CustomScreenTestImpl implements McpScreenTest { |
| 264 | long startTime = System.currentTimeMillis() | 266 | long startTime = System.currentTimeMillis() |
| 265 | 267 | ||
| 266 | // parse the screenPath | 268 | // parse the screenPath |
| 267 | ArrayList<String> screenPathList = ScreenUrlInfo.parseSubScreenPath(csti.rootScreenDef, csti.baseScreenDef, | 269 | def screenPathList |
| 268 | csti.baseScreenPathList, stri.screenPath, stri.parameters, csti.sfi) | 270 | // Special handling for non-webroot root screens with subscreens |
| 271 | if (csti.rootScreenLocation != null && !csti.rootScreenLocation.contains("webroot.xml") && stri.screenPath.contains('/')) { | ||
| 272 | // For non-webroot roots with subscreens, build path list directly | ||
| 273 | // rootScreenDef is the parent screen, screenPath is the subscreen path | ||
| 274 | screenPathList = new ArrayList<>() | ||
| 275 | // Add root screen path (already a full component:// path) | ||
| 276 | screenPathList.add(csti.rootScreenDef.location) | ||
| 277 | // Add subscreen path segments | ||
| 278 | String[] pathSegments = stri.screenPath.split('/') | ||
| 279 | for (String segment in pathSegments) { | ||
| 280 | if (segment && segment.trim().length() > 0) { | ||
| 281 | screenPathList.add(segment) | ||
| 282 | } | ||
| 283 | } | ||
| 284 | logger.info("Custom screen path parsing for non-webroot root: ${screenPathList}") | ||
| 285 | } else { | ||
| 286 | screenPathList = ScreenUrlInfo.parseSubScreenPath(csti.rootScreenDef, csti.baseScreenDef, | ||
| 287 | csti.baseScreenPathList, stri.screenPath, stri.parameters, csti.sfi) | ||
| 288 | } | ||
| 269 | if (screenPathList == null) throw new BaseArtifactException("Could not find screen path ${stri.screenPath} under base screen ${csti.baseScreenDef.location}") | 289 | if (screenPathList == null) throw new BaseArtifactException("Could not find screen path ${stri.screenPath} under base screen ${csti.baseScreenDef.location}") |
| 270 | 290 | ||
| 271 | // push the context | 291 | // push the context |
| ... | @@ -292,7 +312,7 @@ class CustomScreenTestImpl implements McpScreenTest { | ... | @@ -292,7 +312,7 @@ class CustomScreenTestImpl implements McpScreenTest { |
| 292 | } | 312 | } |
| 293 | 313 | ||
| 294 | // set the screenPath | 314 | // set the screenPath |
| 295 | screenRender.screenPath(screenPathList) | 315 | screenRender.screenPath(screenPathList as java.util.List<String>) |
| 296 | 316 | ||
| 297 | // do the render | 317 | // do the render |
| 298 | try { | 318 | try { | ... | ... |
| ... | @@ -862,8 +862,13 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -862,8 +862,13 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 862 | logger.error("Enhanced MCP service ${serviceName} returned null result") | 862 | logger.error("Enhanced MCP service ${serviceName} returned null result") |
| 863 | return [error: "Service returned null result"] | 863 | return [error: "Service returned null result"] |
| 864 | } | 864 | } |
| 865 | // Service framework returns result in 'result' field, but also might return the result directly | 865 | // Service framework returns result in 'result' field when out-parameters are used |
| 866 | return result.result ?: result ?: [error: "Service returned invalid result"] | 866 | // Unwrap the Moqui service result to avoid double nesting in JSON-RPC response |
| 867 | if (result?.containsKey('result')) { | ||
| 868 | return result.result ?: [error: "Service returned empty result"] | ||
| 869 | } else { | ||
| 870 | return result ?: [error: "Service returned null result"] | ||
| 871 | } | ||
| 867 | } catch (Exception e) { | 872 | } catch (Exception e) { |
| 868 | logger.error("Error calling Enhanced MCP service ${serviceName}", e) | 873 | logger.error("Error calling Enhanced MCP service ${serviceName}", e) |
| 869 | return [error: e.message] | 874 | return [error: e.message] | ... | ... |
-
Please register or sign in to post a comment