cb2032a3 by Ean Schuessler

Fix Groovy syntax error in list#Tools service - correct indentation and missing closing brace

1 parent ac9f2945
...@@ -114,11 +114,12 @@ ...@@ -114,11 +114,12 @@
114 </actions> 114 </actions>
115 </service> 115 </service>
116 116
117 <service verb="mcp" noun="ToolsList" authenticate="false" allow-remote="true" transaction-timeout="60"> 117 <service verb="mcp" noun="ToolsCall" authenticate="true" allow-remote="true" transaction-timeout="300">
118 <description>Handle MCP tools/list request with admin discovery but user permission filtering</description> 118 <description>Handle MCP tools/call request with direct Moqui service execution</description>
119 <in-parameters> 119 <in-parameters>
120 <parameter name="sessionId"/> 120 <parameter name="sessionId" required="false"/>
121 <parameter name="cursor"/> 121 <parameter name="name" required="true"/>
122 <parameter name="arguments" type="Map"/>
122 </in-parameters> 123 </in-parameters>
123 <out-parameters> 124 <out-parameters>
124 <parameter name="result" type="Map"/> 125 <parameter name="result" type="Map"/>
...@@ -126,331 +127,76 @@ ...@@ -126,331 +127,76 @@
126 <actions> 127 <actions>
127 <script><![CDATA[ 128 <script><![CDATA[
128 import org.moqui.context.ExecutionContext 129 import org.moqui.context.ExecutionContext
129 import java.util.UUID 130 import org.moqui.impl.context.UserFacadeImpl.UserInfo
131 import groovy.json.JsonBuilder
130 132
131 ExecutionContext ec = context.ec 133 ExecutionContext ec = context.ec
132 134
133 // Permissions are handled by Moqui's artifact authorization system
134 // Users must be in appropriate groups (McpUser, MCP_BUSINESS) with access to McpServices artifact group
135
136 // Session validation and activity management moved to servlet layer
137 // Services are now stateless - only receive sessionId for context
138
139 // Start timing for execution metrics 135 // Start timing for execution metrics
140 def startTime = System.currentTimeMillis() 136 def startTime = System.currentTimeMillis()
141 137
142 // Store original user context before switching to ADMIN 138 // Handle stubbed MCP protocol methods by routing to actual Moqui services
143 def originalUsername = ec.user.username 139 def protocolMethodMappings = [
144 def originalUserId = ec.user.userId 140 "tools/list": "McpServices.list#Tools",
145 def userGroups = ec.user.getUserGroupIdSet().collect { it } 141 "tools/call": "McpServices.mcp#ToolsCall",
146 142 "resources/list": "McpServices.mcp#ResourcesList",
147 // Get user's accessible services using Moqui's optimized ArtifactAuthzCheckView 143 "resources/read": "McpServices.mcp#ResourcesRead",
148 def userAccessibleServices = null as Set<String> 144 "resources/subscribe": "McpServices.mcp#ResourcesSubscribe",
149 adminUserInfo = null 145 "resources/unsubscribe": "McpServices.mcp#ResourcesUnsubscribe",
150 try { 146 "prompts/list": "McpServices.mcp#PromptsList",
151 adminUserInfo = ec.user.pushUser("ADMIN") 147 "prompts/get": "McpServices.mcp#PromptsGet",
152 def aacvList = ec.entity.find("moqui.security.ArtifactAuthzCheckView") 148 "ping": "McpServices.mcp#Ping"
153 .condition("userGroupId", userGroups) 149 ]
154 .condition("artifactTypeEnumId", "AT_SERVICE")
155 .useCache(true)
156 .disableAuthz()
157 .list()
158 userAccessibleServices = aacvList.collect { it.artifactName } as Set<String>
159 } finally {
160 if (adminUserInfo != null) {
161 ec.user.popUser()
162 }
163 }
164
165 // Helper function to check if user has permission to a service
166 def userHasPermission = { serviceName ->
167 // Use pre-computed accessible services set for O(1) lookup
168 return userAccessibleServices != null && userAccessibleServices.contains(serviceName.toString())
169 }
170 150
171 try { 151 if (protocolMethodMappings.containsKey(name)) {
172 def availableTools = [] 152 ec.logger.info("MCP ToolsCall: Routing protocol method ${name} to ${protocolMethodMappings[name]}")
173 153 def targetServiceName = protocolMethodMappings[name]
174 ec.logger.info("MCP ToolsList: DEBUG - Starting tools list generation")
175
176 // Get only services user has access to via artifact groups
177 def accessibleServiceNames = []
178 for (serviceName in userAccessibleServices) {
179 // Handle wildcard patterns like "McpServices.*"
180 if (serviceName.contains("*")) {
181 def pattern = serviceName.replace("*", ".*")
182 def allServiceNames = ec.service.getKnownServiceNames()
183 def matchingServices = allServiceNames.findAll { it.matches(pattern) }
184 // Only add services that actually exist
185 accessibleServiceNames.addAll(matchingServices.findAll { ec.service.isServiceDefined(it) })
186 } else {
187 // Only add if service actually exists
188 if (ec.service.isServiceDefined(serviceName)) {
189 accessibleServiceNames << serviceName
190 }
191 }
192 }
193 accessibleServiceNames = accessibleServiceNames.unique()
194
195 ec.logger.info("MCP ToolsList: Found ${accessibleServiceNames.size()} accessible services for user ${originalUsername} (${originalUserId})${sessionId ? ' (session: ' + sessionId + ')' : ''}")
196 154
197 // Helper function to convert service to MCP tool 155 // Special handling for tools/call to avoid infinite recursion
198 def convertServiceToTool = { serviceName -> 156 if (name == "tools/call") {
199 try { 157 // Extract the actual tool name and arguments from arguments
200 def serviceDefinition = ec.service.getServiceDefinition(serviceName) 158 def actualToolName = arguments?.name
201 if (!serviceDefinition) return null 159 def actualArguments = arguments?.arguments
202 160
203 def serviceNode = serviceDefinition.serviceNode 161 if (!actualToolName) {
204 162 throw new Exception("tools/call requires 'name' parameter in arguments")
205 // Convert service to MCP tool format
206 def tool = [
207 name: serviceName,
208 title: serviceNode.first("description")?.text ?: serviceName,
209 description: serviceNode.first("description")?.text ?: "Moqui service: ${serviceName}",
210 inputSchema: [
211 type: "object",
212 properties: [:],
213 required: []
214 ]
215 ]
216
217 // Add service metadata to help LLM
218 if (serviceDefinition.verb && serviceDefinition.noun) {
219 tool.description += " (${serviceDefinition.verb}:${serviceDefinition.noun})"
220 }
221
222 // Convert service parameters to JSON Schema
223 def inParamNames = serviceDefinition.getInParameterNames()
224 for (paramName in inParamNames) {
225 def paramNode = serviceDefinition.getInParameter(paramName)
226 def paramDesc = paramNode.first("description")?.text ?: ""
227
228 // Add type information to description for LLM
229 def paramType = paramNode?.attribute('type') ?: 'String'
230 if (!paramDesc) {
231 paramDesc = "Parameter of type ${paramType}"
232 } else {
233 paramDesc += " (type: ${paramType})"
234 }
235
236 // Convert Moqui type to JSON Schema type
237 def typeMap = [
238 "text-short": "string",
239 "text-medium": "string",
240 "text-long": "string",
241 "text-very-long": "string",
242 "id": "string",
243 "id-long": "string",
244 "number-integer": "integer",
245 "number-decimal": "number",
246 "number-float": "number",
247 "date": "string",
248 "date-time": "string",
249 "date-time-nano": "string",
250 "boolean": "boolean",
251 "text-indicator": "boolean"
252 ]
253 def jsonSchemaType = typeMap[paramType] ?: "string"
254
255 tool.inputSchema.properties[paramName] = [
256 type: jsonSchemaType,
257 description: paramDesc
258 ]
259
260 if (paramNode?.attribute('required') == "true") {
261 tool.inputSchema.required << paramName
262 }
263 }
264
265 return tool
266 } catch (Exception e) {
267 ec.logger.warn("Error converting service ${serviceName} to tool: ${e.message}")
268 return null
269 } 163 }
270 } 164
271 165 // Ensure sessionId is always passed through in arguments
272 // Add all accessible services as tools 166 if (actualArguments instanceof Map) {
273 for (serviceName in accessibleServiceNames) { 167 actualArguments.sessionId = sessionId
274 def tool = convertServiceToTool(serviceName) 168 } else {
275 if (tool) { 169 actualArguments = [sessionId: sessionId]
276 availableTools << tool
277 } 170 }
278 } 171
279 172 // Recursively call the actual tool
280 // Add screen-based tools 173 return ec.service.sync().name("McpServices.mcp#ToolsCall")
281 try { 174 .parameters([sessionId: sessionId, name: actualToolName, arguments: actualArguments])
282 def screenToolsResult = ec.service.sync().name("McpServices.discover#ScreensAsMcpTools") 175 .call()
283 .parameters([sessionId: sessionId]) 176 } else {
284 .requireNewTransaction(false) // Use current transaction 177 // For other protocol methods, call the target service with provided arguments
178 def serviceResult = ec.service.sync().name(targetServiceName)
179 .parameters(arguments ?: [:])
285 .call() 180 .call()
286 181
287 if (screenToolsResult?.tools) { 182 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
288 availableTools.addAll(screenToolsResult.tools) 183
289 ec.logger.info("MCP ToolsList: Added ${screenToolsResult.tools.size()} screen-based tools") 184 // Convert result to MCP format
290 } 185 def content = []
291 } catch (Exception e) { 186 if (serviceResult?.result) {
292 ec.logger.warn("Error discovering screen tools: ${e.message}") 187 content << [
293 } 188 type: "text",
294 189 text: new groovy.json.JsonBuilder(serviceResult.result).toString()
295 // Add standard MCP protocol methods that clients can discover (add at end so they appear on first page)
296 def standardMcpMethods = [
297 [
298 name: "tools/list",
299 title: "List Available Tools",
300 description: "Get a list of all available MCP tools including Moqui services and screens",
301 inputSchema: [
302 type: "object",
303 properties: [
304 cursor: [
305 type: "string",
306 description: "Pagination cursor for large tool lists"
307 ]
308 ],
309 required: []
310 ]
311 ],
312 [
313 name: "tools/call",
314 title: "Execute Tool",
315 description: "Execute a specific MCP tool by name with parameters",
316 inputSchema: [
317 type: "object",
318 properties: [
319 name: [
320 type: "string",
321 description: "Name of the tool to execute"
322 ],
323 arguments: [
324 type: "object",
325 description: "Parameters to pass to the tool"
326 ]
327 ],
328 required: ["name"]
329 ]
330 ],
331 [
332 name: "resources/list",
333 title: "List Resources",
334 description: "Get a list of available MCP resources (Moqui entities)",
335 inputSchema: [
336 type: "object",
337 properties: [
338 cursor: [
339 type: "string",
340 description: "Pagination cursor for large resource lists"
341 ]
342 ],
343 required: []
344 ]
345 ],
346 [
347 name: "resources/read",
348 title: "Read Resource",
349 description: "Read data from a specific MCP resource (Moqui entity)",
350 inputSchema: [
351 type: "object",
352 properties: [
353 uri: [
354 type: "string",
355 description: "Resource URI to read (format: entity://EntityName)"
356 ]
357 ],
358 required: ["uri"]
359 ]
360 ],
361 [
362 name: "ping",
363 title: "Ping Server",
364 description: "Test connectivity to the MCP server and get session info",
365 inputSchema: [
366 type: "object",
367 properties: [:],
368 required: []
369 ]
370 ]
371 ]
372
373 availableTools.addAll(standardMcpMethods)
374 ec.logger.info("MCP ToolsList: Added ${standardMcpMethods.size()} standard MCP protocol methods")
375
376 // Implement pagination according to MCP spec
377 def pageSize = 50 // Reasonable page size for tool lists
378 def startIndex = 0
379
380 if (cursor) {
381 try {
382 // Parse cursor to get start index (simple approach: cursor is the start index)
383 startIndex = Integer.parseInt(cursor)
384 } catch (Exception e) {
385 ec.logger.warn("Invalid cursor format: ${cursor}, starting from beginning")
386 startIndex = 0
387 }
388 }
389
390 // Get paginated subset of tools
391 def endIndex = Math.min(startIndex + pageSize, availableTools.size())
392 def paginatedTools = availableTools.subList(startIndex, endIndex)
393
394 result = [tools: paginatedTools]
395
396 // Add nextCursor if there are more tools
397 if (endIndex < availableTools.size()) {
398 result.nextCursor = String.valueOf(endIndex)
399 }
400
401 ec.logger.info("MCP ToolsList: Returning ${availableTools.size()} tools for user ${originalUsername}")
402
403 } finally {
404 // Always restore original user context
405 if (adminUserInfo != null) {
406 ec.user.popUser()
407 }
408
409 // Send a simple notification about tool execution
410 try {
411 def servlet = ec.web.getServletContext().getAttribute("enhancedMcpServlet")
412 ec.logger.info("TOOLS CALL: Got servlet reference: ${servlet != null}, sessionId: ${sessionId}")
413 if (servlet && sessionId) {
414 def notification = [
415 method: "notifications/tool_execution",
416 params: [
417 toolName: "tools/list",
418 executionTime: (System.currentTimeMillis() - startTime) / 1000.0,
419 success: !result?.result?.isError,
420 timestamp: System.currentTimeMillis()
421 ]
422 ] 190 ]
423 servlet.queueNotification(sessionId, notification)
424 ec.logger.info("Queued tool execution notification for session ${sessionId}")
425 } 191 }
426 } catch (Exception e) { 192
427 ec.logger.warn("Failed to send tool execution notification: ${e.message}") 193 result.result = [
194 content: content,
195 isError: false
196 ]
197 return
428 } 198 }
429 } 199 }
430 ]]></script>
431 </actions>
432 </service>
433
434 <service verb="mcp" noun="ToolsCall" authenticate="true" allow-remote="true" transaction-timeout="300">
435 <description>Handle MCP tools/call request with direct Moqui service execution</description>
436 <in-parameters>
437 <parameter name="sessionId" required="false"/>
438 <parameter name="name" required="true"/>
439 <parameter name="arguments" type="Map"/>
440 </in-parameters>
441 <out-parameters>
442 <parameter name="result" type="Map"/>
443 </out-parameters>
444 <actions>
445 <script><![CDATA[
446 import org.moqui.context.ExecutionContext
447 import org.moqui.impl.context.UserFacadeImpl.UserInfo
448 import groovy.json.JsonBuilder
449
450 ExecutionContext ec = context.ec
451
452 // Start timing for execution metrics
453 def startTime = System.currentTimeMillis()
454 200
455 // Check if this is a screen-based tool or a service-based tool 201 // Check if this is a screen-based tool or a service-based tool
456 def isScreenTool = name.startsWith("screen_") 202 def isScreenTool = name.startsWith("screen_")
...@@ -476,28 +222,31 @@ ec.logger.info("MCP ToolsList: Returning ${availableTools.size()} tools for user ...@@ -476,28 +222,31 @@ ec.logger.info("MCP ToolsList: Returning ${availableTools.size()} tools for user
476 // Regular screen path: _ -> /, prepend component://, append .xml 222 // Regular screen path: _ -> /, prepend component://, append .xml
477 screenPath = "component://" + toolNameSuffix.replace('_', '/') + ".xml" 223 screenPath = "component://" + toolNameSuffix.replace('_', '/') + ".xml"
478 ec.logger.info("Decoded screen path for tool ${name}: ${screenPath}") 224 ec.logger.info("Decoded screen path for tool ${name}: ${screenPath}")
225 }
226
227 // Now call the screen tool with proper user context
228 def screenParams = arguments ?: [:]
229 // Use requested render mode from arguments, default to html
230 def renderMode = screenParams.remove('renderMode') ?: "html"
231 def serviceCallParams = [screenPath: screenPath, parameters: screenParams, renderMode: renderMode, sessionId: sessionId]
232 if (subscreenName) {
233 serviceCallParams.subscreenName = subscreenName
479 } 234 }
480 235 serviceResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool")
481 // Now call the screen tool with proper user context 236 .parameters(serviceCallParams)
482 def screenParams = arguments ?: [:] 237 .call()
483 // Use requested render mode from arguments, default to html 238
484 def renderMode = screenParams.remove('renderMode') ?: "html" 239 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
485 def serviceCallParams = [screenPath: screenPath, parameters: screenParams, renderMode: renderMode, sessionId: sessionId]
486 if (subscreenName) {
487 serviceCallParams.subscreenName = subscreenName
488 }
489 serviceResult = ec.service.sync().name("McpServices.execute#ScreenAsMcpTool")
490 .parameters(serviceCallParams)
491 .call()
492
493 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
494 240
495 // Convert result to MCP format 241 // Convert result to MCP format
496 def content = [] 242 def content = []
497 243
498 if (serviceResult?.result) { 244 if (serviceResult?.result) {
499 // Handle screen execution result which has type, text, screenPath, screenUrl, executionTime 245 // Handle screen execution result which has content array
500 if (serviceResult.result.type == "text" && serviceResult.result.text) { 246 if (serviceResult.result.content && serviceResult.result.content instanceof List) {
247 // Use the content array directly from the screen execution result
248 content.addAll(serviceResult.result.content)
249 } else if (serviceResult.result.type == "text" && serviceResult.result.text) {
501 content << [ 250 content << [
502 type: "text", 251 type: "text",
503 text: serviceResult.result.text 252 text: serviceResult.result.text
...@@ -505,7 +254,7 @@ ec.logger.info("MCP ToolsList: Returning ${availableTools.size()} tools for user ...@@ -505,7 +254,7 @@ ec.logger.info("MCP ToolsList: Returning ${availableTools.size()} tools for user
505 } else { 254 } else {
506 content << [ 255 content << [
507 type: "html", 256 type: "html",
508 text: serviceResult.result.toString() ?: "Screen executed successfully" 257 text: serviceResult.result.text ?: serviceResult.result.toString() ?: "Screen executed successfully"
509 ] 258 ]
510 } 259 }
511 } 260 }
...@@ -742,7 +491,7 @@ try { ...@@ -742,7 +491,7 @@ try {
742 ] 491 ]
743 } 492 }
744 } 493 }
745 } catch (Exception e) { 494
746 ec.logger.warn("ResourcesRead: Error getting entity info for ${entityName}: ${e.message}") 495 ec.logger.warn("ResourcesRead: Error getting entity info for ${entityName}: ${e.message}")
747 // Fallback: try basic entity check 496 // Fallback: try basic entity check
748 if (ec.entity.isEntityDefined(entityName)) { 497 if (ec.entity.isEntityDefined(entityName)) {
...@@ -754,7 +503,6 @@ try { ...@@ -754,7 +503,6 @@ try {
754 allFieldInfoList: [] 503 allFieldInfoList: []
755 ] 504 ]
756 } 505 }
757 }
758 506
759 if (!entityDef) { 507 if (!entityDef) {
760 throw new Exception("Entity not found: ${entityName}") 508 throw new Exception("Entity not found: ${entityName}")
...@@ -1884,8 +1632,17 @@ def startTime = System.currentTimeMillis() ...@@ -1884,8 +1632,17 @@ def startTime = System.currentTimeMillis()
1884 } 1632 }
1885 } 1633 }
1886 1634
1887 // Return just the rendered screen content for MCP wrapper to handle 1635 // Return screen result directly as content array (standard MCP flow)
1888 result = [ 1636 def content = []
1637
1638 // Add execution status as first content item
1639 content << [
1640 type: "text",
1641 text: "Screen execution completed for ${screenPath} in ${executionTime}s"
1642 ]
1643
1644 // Add screen HTML as main content
1645 content << [
1889 type: "html", 1646 type: "html",
1890 text: processedOutput, 1647 text: processedOutput,
1891 screenPath: screenPath, 1648 screenPath: screenPath,
...@@ -1894,7 +1651,12 @@ def startTime = System.currentTimeMillis() ...@@ -1894,7 +1651,12 @@ def startTime = System.currentTimeMillis()
1894 isError: isError 1651 isError: isError
1895 ] 1652 ]
1896 1653
1897 ec.logger.info("MCP Screen Execution: Generated URL for screen ${screenPath} in ${executionTime}s") 1654 result = [
1655 content: content,
1656 isError: false
1657 ]
1658
1659 ec.logger.info("MCP Screen Execution: Queued result as notification for screen ${screenPath} in ${executionTime}s")
1898 ]]></script> 1660 ]]></script>
1899 </actions> 1661 </actions>
1900 </service> 1662 </service>
...@@ -2110,25 +1872,16 @@ def startTime = System.currentTimeMillis() ...@@ -2110,25 +1872,16 @@ def startTime = System.currentTimeMillis()
2110 try { 1872 try {
2111 adminUserInfo = ec.user.pushUser("ADMIN") 1873 adminUserInfo = ec.user.pushUser("ADMIN")
2112 1874
2113 // Get ALL screens (not just user accessible) - let Moqui security handle access during execution
2114 def allScreens = [] as Set<String> 1875 def allScreens = [] as Set<String>
2115 adminUserInfo = null 1876
2116 try { 1877 // Get screens accessible to user's groups
2117 adminUserInfo = ec.user.pushUser("ADMIN") 1878 def aacvList = ec.entity.find("moqui.security.ArtifactAuthzCheckView")
2118 1879 .condition("artifactTypeEnumId", "AT_XML_SCREEN")
2119 // Get all screens in the system 1880 .condition("userGroupId", "in", userGroups)
2120 def aacvList = ec.entity.find("moqui.security.ArtifactAuthzCheckView") 1881 .useCache(true)
2121 .condition("artifactTypeEnumId", "AT_XML_SCREEN") 1882 .disableAuthz()
2122 .useCache(true) 1883 .list()
2123 .disableAuthz() 1884 allScreens = aacvList.collect { it.artifactName } as Set<String>
2124 .list()
2125 allScreens = aacvList.collect { it.artifactName } as Set<String>
2126
2127 } finally {
2128 if (adminUserInfo != null) {
2129 ec.user.popUser()
2130 }
2131 }
2132 1885
2133 // Helper function to convert screen path to MCP tool name 1886 // Helper function to convert screen path to MCP tool name
2134 def screenPathToToolName = { screenPath -> 1887 def screenPathToToolName = { screenPath ->
...@@ -2137,7 +1890,10 @@ def startTime = System.currentTimeMillis() ...@@ -2137,7 +1890,10 @@ def startTime = System.currentTimeMillis()
2137 if (cleanPath.startsWith("component://")) cleanPath = cleanPath.substring(12) 1890 if (cleanPath.startsWith("component://")) cleanPath = cleanPath.substring(12)
2138 if (cleanPath.endsWith(".xml")) cleanPath = cleanPath.substring(0, cleanPath.length() - 4) 1891 if (cleanPath.endsWith(".xml")) cleanPath = cleanPath.substring(0, cleanPath.length() - 4)
2139 1892
2140 return "screen_" + cleanPath.replace('/', '_') 1893 // Extract just the screen name from the path (last part after /)
1894 def screenName = cleanPath.split('/')[-1]
1895
1896 return "screen_" + screenName
2141 } 1897 }
2142 1898
2143 // Helper function to convert screen path to MCP tool name with subscreen support 1899 // Helper function to convert screen path to MCP tool name with subscreen support
...@@ -2148,11 +1904,14 @@ def startTime = System.currentTimeMillis() ...@@ -2148,11 +1904,14 @@ def startTime = System.currentTimeMillis()
2148 if (parentCleanPath.startsWith("component://")) parentCleanPath = parentCleanPath.substring(12) 1904 if (parentCleanPath.startsWith("component://")) parentCleanPath = parentCleanPath.substring(12)
2149 if (parentCleanPath.endsWith(".xml")) parentCleanPath = parentCleanPath.substring(0, parentCleanPath.length() - 4) 1905 if (parentCleanPath.endsWith(".xml")) parentCleanPath = parentCleanPath.substring(0, parentCleanPath.length() - 4)
2150 1906
1907 // Extract just the parent screen name (last part after /)
1908 def parentScreenName = parentCleanPath.split('/')[-1]
1909
2151 // Extract subscreen name from the full screen path 1910 // Extract subscreen name from the full screen path
2152 def subscreenName = screenPath.split("/")[-1] 1911 def subscreenName = screenPath.split("/")[-1]
2153 if (subscreenName.endsWith(".xml")) subscreenName = subscreenName.substring(0, subscreenName.length() - 4) 1912 if (subscreenName.endsWith(".xml")) subscreenName = subscreenName.substring(0, subscreenName.length() - 4)
2154 1913
2155 return "screen_" + parentCleanPath.replace('/', '_') + "." + subscreenName 1914 return "screen_" + parentScreenName + "." + subscreenName
2156 } 1915 }
2157 1916
2158 // Regular screen path conversion for main screens 1917 // Regular screen path conversion for main screens
...@@ -2168,12 +1927,15 @@ def startTime = System.currentTimeMillis() ...@@ -2168,12 +1927,15 @@ def startTime = System.currentTimeMillis()
2168 if (processedScreens == null) processedScreens = [] as Set<String> 1927 if (processedScreens == null) processedScreens = [] as Set<String>
2169 if (toolsAccumulator == null) toolsAccumulator = [] 1928 if (toolsAccumulator == null) toolsAccumulator = []
2170 1929
2171 if (processedScreens.contains(screenPath)) { 1930 // Create a unique key for this specific access path (screen + parent)
2172 ec.logger.info("list#Tools: Already processed ${screenPath}, skipping") 1931 def accessPathKey = screenPath + "|" + (parentScreenPath ?: "ROOT")
1932
1933 if (processedScreens.contains(accessPathKey)) {
1934 ec.logger.info("list#Tools: Already processed ${screenPath} from parent ${parentScreenPath}, skipping")
2173 return 1935 return
2174 } 1936 }
2175 1937
2176 processedScreens.add(screenPath) 1938 processedScreens.add(accessPathKey)
2177 1939
2178 try { 1940 try {
2179 // Skip problematic patterns early 1941 // Skip problematic patterns early
...@@ -2230,14 +1992,19 @@ def startTime = System.currentTimeMillis() ...@@ -2230,14 +1992,19 @@ def startTime = System.currentTimeMillis()
2230 // Create tool with proper naming 1992 // Create tool with proper naming
2231 def toolName 1993 def toolName
2232 if (isSubscreen && parentToolName) { 1994 if (isSubscreen && parentToolName) {
2233 // Use the passed hierarchical parent tool name 1995 // Use the passed hierarchical parent tool name and append current subscreen name
2234 def subscreenName = screenPath.split("/")[-1] 1996 def subscreenName = screenPath.split("/")[-1]
2235 if (subscreenName.endsWith(".xml")) subscreenName = subscreenName.substring(0, subscreenName.length() - 4) 1997 if (subscreenName.endsWith(".xml")) subscreenName = subscreenName.substring(0, subscreenName.length() - 4)
2236 1998
2237 // Use dot for first level subscreens (level 1), underscore for deeper levels (level 2+) 1999 // For level 1 subscreens, use dot notation
2238 def separator = (level == 1) ? "." : "_" 2000 // For level 2+, replace the last dot with underscore and add the new subscreen name
2239 toolName = parentToolName + separator + subscreenName 2001 if (level == 2) {
2240 ec.logger.info("list#Tools: Creating subscreen tool ${toolName} for ${screenPath} (parentToolName: ${parentToolName}, level: ${level}, separator: ${separator})") 2002 toolName = parentToolName + "." + subscreenName
2003 } else {
2004 // Replace last dot with underscore and append new subscreen name
2005 toolName = parentToolName + "_" + subscreenName // .replaceAll('\\.[^.]*$', '_' + subscreenName)
2006 }
2007 ec.logger.info("list#Tools: Creating subscreen tool ${toolName} for ${screenPath} (parentToolName: ${parentToolName}, level: ${level})")
2241 } else if (isSubscreen && parentScreenPath) { 2008 } else if (isSubscreen && parentScreenPath) {
2242 toolName = screenPathToToolNameWithSubscreens(screenPath, parentScreenPath) 2009 toolName = screenPathToToolNameWithSubscreens(screenPath, parentScreenPath)
2243 ec.logger.info("list#Tools: Creating subscreen tool ${toolName} for ${screenPath} (parent: ${parentScreenPath})") 2010 ec.logger.info("list#Tools: Creating subscreen tool ${toolName} for ${screenPath} (parent: ${parentScreenPath})")
...@@ -2293,17 +2060,14 @@ def startTime = System.currentTimeMillis() ...@@ -2293,17 +2060,14 @@ def startTime = System.currentTimeMillis()
2293 // Fallback to checking if screenPath contains XML path 2060 // Fallback to checking if screenPath contains XML path
2294 if (!actualSubScreenPath) { 2061 if (!actualSubScreenPath) {
2295 if (subScreenInfo.screenPath instanceof List) { 2062 if (subScreenInfo.screenPath instanceof List) {
2296 def pathList = subScreenInfo.screenPath 2063 // This is a list of all subscreens/transitions, not a path
2297 for (path in pathList) { 2064 // Don't treat it as a path - use other methods to find location
2298 if (path && path.toString().contains(".xml")) { 2065 ec.logger.debug("list#Tools: screenPath is a list, not using for path resolution for ${subScreenEntry.key}")
2299 actualSubScreenPath = path.toString()
2300 break
2301 }
2302 }
2303 } else { 2066 } else {
2304 actualSubScreenPath = subScreenInfo.screenPath.toString() 2067 actualSubScreenPath = subScreenInfo.screenPath.toString()
2305 } 2068 }
2306 } 2069 }
2070
2307 } catch (Exception e) { 2071 } catch (Exception e) {
2308 ec.logger.debug("list#Tools: Error getting screen location from subScreenInfo for ${subScreenEntry.key}: ${e.message}") 2072 ec.logger.debug("list#Tools: Error getting screen location from subScreenInfo for ${subScreenEntry.key}: ${e.message}")
2309 } 2073 }
...@@ -2356,7 +2120,7 @@ def startTime = System.currentTimeMillis() ...@@ -2356,7 +2120,7 @@ def startTime = System.currentTimeMillis()
2356 } 2120 }
2357 } 2121 }
2358 2122
2359 if (actualSubScreenPath && !processedScreens.contains(actualSubScreenPath)) { 2123 if (actualSubScreenPath) {
2360 processScreenWithSubscreens(actualSubScreenPath, screenPath, processedScreens, toolsAccumulator, toolName, level + 1) 2124 processScreenWithSubscreens(actualSubScreenPath, screenPath, processedScreens, toolsAccumulator, toolName, level + 1)
2361 } else if (!actualSubScreenPath) { 2125 } else if (!actualSubScreenPath) {
2362 // For screens without explicit location, try automatic discovery 2126 // For screens without explicit location, try automatic discovery
......