McpToolAdapter.groovy 7.39 KB
/*
 * This software is in the public domain under CC0 1.0 Universal plus a
 * Grant of Patent License.
 *
 * To the extent possible under law, author(s) have dedicated all
 * copyright and related and neighboring rights to this software to the
 * public domain worldwide. This software is distributed without any
 * warranty.
 *
 * You should have received a copy of the CC0 Public Domain Dedication
 * along with this software (see the LICENSE.md file). If not, see
 * <http://creativecommons.org/publicdomain/zero/1.0/>.
 */
package org.moqui.mcp.adapter

import org.moqui.context.ExecutionContext
import org.slf4j.Logger
import org.slf4j.LoggerFactory

/**
 * Adapter that maps MCP tool calls to Moqui services.
 * Provides a clean translation layer between MCP protocol and Moqui service framework.
 */
class McpToolAdapter {
    protected final static Logger logger = LoggerFactory.getLogger(McpToolAdapter.class)

    // MCP tool name → Moqui service name mapping
    private static final Map<String, String> TOOL_SERVICE_MAP = [
        'moqui_browse_screens': 'McpServices.mcp#BrowseScreens',
        'moqui_search_screens': 'McpServices.mcp#SearchScreens',
        'moqui_get_screen_details': 'McpServices.mcp#GetScreenDetails',
        'moqui_get_help': 'McpServices.mcp#GetHelp'
    ]

    // MCP method → Moqui service name mapping for JSON-RPC methods
    private static final Map<String, String> METHOD_SERVICE_MAP = [
        'initialize': 'McpServices.mcp#Initialize',
        'ping': 'McpServices.mcp#Ping',
        'tools/list': 'McpServices.list#Tools',
        'tools/call': 'McpServices.mcp#ToolsCall',
        'resources/list': 'McpServices.mcp#ResourcesList',
        'resources/read': 'McpServices.mcp#ResourcesRead',
        'resources/templates/list': 'McpServices.mcp#ResourcesTemplatesList',
        'resources/subscribe': 'McpServices.mcp#ResourcesSubscribe',
        'resources/unsubscribe': 'McpServices.mcp#ResourcesUnsubscribe',
        'prompts/list': 'McpServices.mcp#PromptsList',
        'prompts/get': 'McpServices.mcp#PromptsGet',
        'roots/list': 'McpServices.mcp#RootsList',
        'sampling/createMessage': 'McpServices.mcp#SamplingCreateMessage',
        'elicitation/create': 'McpServices.mcp#ElicitationCreate'
    ]

    // Tool descriptions for MCP tool definitions
    private static final Map<String, String> TOOL_DESCRIPTIONS = [
        'moqui_browse_screens': 'Browse Moqui screen hierarchy and render screen content',
        'moqui_search_screens': 'Search for screens by name to find their paths',
        'moqui_get_screen_details': 'Get screen field details including dropdown options',
        'moqui_get_help': 'Fetch extended documentation for a screen or service'
    ]

    /**
     * Call an MCP tool, translating to the appropriate Moqui service
     * @param ec The execution context
     * @param toolName The MCP tool name
     * @param arguments The tool arguments
     * @return The result map or error map
     */
    Map callTool(ExecutionContext ec, String toolName, Map arguments) {
        String serviceName = TOOL_SERVICE_MAP.get(toolName)
        if (!serviceName) {
            logger.warn("Unknown tool: ${toolName}")
            return [error: [code: -32601, message: "Unknown tool: ${toolName}"]]
        }

        logger.debug("Calling tool ${toolName} -> service ${serviceName} with args: ${arguments}")

        try {
            ec.artifactExecution.disableAuthz()
            def result = ec.service.sync()
                .name(serviceName)
                .parameters(arguments ?: [:])
                .call()

            logger.debug("Tool ${toolName} completed successfully")

            // Extract result from service response if wrapped
            if (result?.containsKey('result')) {
                return result.result
            }
            return result ?: [:]

        } catch (Exception e) {
            logger.error("Error calling tool ${toolName}: ${e.message}", e)
            return [error: [code: -32000, message: e.message]]
        } finally {
            ec.artifactExecution.enableAuthz()
        }
    }

    /**
     * Call an MCP method, translating to the appropriate Moqui service
     * @param ec The execution context
     * @param method The MCP method name
     * @param params The method parameters
     * @return The result map or error map
     */
    Map callMethod(ExecutionContext ec, String method, Map params) {
        String serviceName = METHOD_SERVICE_MAP.get(method)
        if (!serviceName) {
            logger.warn("Unknown method: ${method}")
            return [error: [code: -32601, message: "Method not found: ${method}"]]
        }

        logger.debug("Calling method ${method} -> service ${serviceName}")

        try {
            ec.artifactExecution.disableAuthz()
            def result = ec.service.sync()
                .name(serviceName)
                .parameters(params ?: [:])
                .call()

            logger.debug("Method ${method} completed successfully")

            // Extract result from service response if wrapped
            if (result?.containsKey('result')) {
                return result.result
            }
            return result ?: [:]

        } catch (Exception e) {
            logger.error("Error calling method ${method}: ${e.message}", e)
            return [error: [code: -32603, message: "Internal error: ${e.message}"]]
        } finally {
            ec.artifactExecution.enableAuthz()
        }
    }

    /**
     * Check if a tool name is valid
     * @param toolName The tool name to check
     * @return true if the tool is known
     */
    boolean isValidTool(String toolName) {
        return TOOL_SERVICE_MAP.containsKey(toolName)
    }

    /**
     * Check if a method name is valid (has a service mapping)
     * @param method The method name to check
     * @return true if the method has a service mapping
     */
    boolean isValidMethod(String method) {
        return METHOD_SERVICE_MAP.containsKey(method)
    }

    /**
     * Get the service name for a given tool
     * @param toolName The tool name
     * @return The service name or null if not found
     */
    String getServiceForTool(String toolName) {
        return TOOL_SERVICE_MAP.get(toolName)
    }

    /**
     * Get the service name for a given method
     * @param method The method name
     * @return The service name or null if not found
     */
    String getServiceForMethod(String method) {
        return METHOD_SERVICE_MAP.get(method)
    }

    /**
     * Get the list of available tools with their definitions
     * @return List of tool definition maps
     */
    List<Map> listTools() {
        return TOOL_SERVICE_MAP.keySet().collect { toolName ->
            [
                name: toolName,
                description: TOOL_DESCRIPTIONS.get(toolName) ?: "MCP tool: ${toolName}",
                serviceName: TOOL_SERVICE_MAP.get(toolName)
            ]
        }
    }

    /**
     * Get tool description
     * @param toolName The tool name
     * @return The tool description or null if not found
     */
    String getToolDescription(String toolName) {
        return TOOL_DESCRIPTIONS.get(toolName)
    }

    /**
     * Get all supported tool names
     * @return Set of tool names
     */
    Set<String> getToolNames() {
        return TOOL_SERVICE_MAP.keySet()
    }

    /**
     * Get all supported method names
     * @return Set of method names
     */
    Set<String> getMethodNames() {
        return METHOD_SERVICE_MAP.keySet()
    }
}