Fix Groovy syntax error in list#Tools service - correct indentation and missing closing brace
Showing
1 changed file
with
135 additions
and
371 deletions
| ... | @@ -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 | ... | ... |
-
Please register or sign in to post a comment