f4695781 by Ean Schuessler

start adding screen resource support

1 parent 1d9ca524
......@@ -23,6 +23,8 @@
<moqui.security.ArtifactGroup artifactGroupId="McpScreenTransitions" description="MCP Screen Transitions"/>
<moqui.security.ArtifactGroup artifactGroupId="McpBusinessServices" description="MCP Essential Business Services"/>
<moqui.security.ArtifactGroup artifactGroupId="McpSecurityEntities" description="Security entities needed for permission checks"/>
<moqui.security.ArtifactGroup artifactGroupId="McpScreens" description="MCP Screen Access"/>
<moqui.security.ArtifactGroup artifactGroupId="McpScreenTools" description="MCP Screen-based Tools"/>
<!-- MCP Artifact Group Members -->
<moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.*" artifactTypeEnumId="AT_SERVICE"/>
......@@ -34,6 +36,24 @@
<moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.mcp#ResourcesList" artifactTypeEnumId="AT_SERVICE"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.mcp#ResourcesRead" artifactTypeEnumId="AT_SERVICE"/>
<!-- Screen Discovery and Execution Services -->
<moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.discover#ScreensAsMcpTools" artifactTypeEnumId="AT_SERVICE"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.convert#ScreenToMcpTool" artifactTypeEnumId="AT_SERVICE"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.execute#ScreenAsMcpTool" artifactTypeEnumId="AT_SERVICE"/>
<!-- Common Screen Access Patterns -->
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/order/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/party/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/invoice/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/product/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/ledger/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/marketing/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/sales/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/manufacturing/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/warehouse/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/humanresource/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpScreens" artifactName="apps/project/*" artifactTypeEnumId="AT_XML_SCREEN"/>
<!-- Essential Business Services -->
<moqui.security.ArtifactGroupMember artifactGroupId="McpBusinessServices" artifactName="mantle.order.OrderServices.create#Order" artifactTypeEnumId="AT_SERVICE"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpBusinessServices" artifactName="mantle.party.PartyServices.find#Party" artifactTypeEnumId="AT_SERVICE"/>
......@@ -80,17 +100,23 @@
<moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpServices" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
<moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpRestPaths" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
<moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpScreenTransitions" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
<moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpScreens" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_VIEW"/>
<moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpScreenTools" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
<!-- Give ALL users access to security entities needed for permission checks -->
<moqui.security.ArtifactAuthz userGroupId="ALL_USERS" artifactGroupId="McpSecurityEntities" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
<!-- Ensure ADMIN user always has access to security entities needed for permission checks -->
<moqui.security.ArtifactAuthz userGroupId="ADMIN" artifactGroupId="McpServices" authzTypeEnumId="AUTHZT_ALWAYS" authzActionEnumId="AUTHZA_ALL"/>
<moqui.security.ArtifactAuthz userGroupId="ADMIN" artifactGroupId="McpScreens" authzTypeEnumId="AUTHZT_ALWAYS" authzActionEnumId="AUTHZA_ALL"/>
<moqui.security.ArtifactAuthz userGroupId="ADMIN" artifactGroupId="McpScreenTools" authzTypeEnumId="AUTHZT_ALWAYS" authzActionEnumId="AUTHZA_ALL"/>
<!-- MCP Business Group Authz -->
<moqui.security.ArtifactAuthz userGroupId="MCP_BUSINESS" artifactGroupId="McpServices" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
<moqui.security.ArtifactAuthz userGroupId="MCP_BUSINESS" artifactGroupId="McpBusinessServices" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
<moqui.security.ArtifactAuthz userGroupId="MCP_BUSINESS" artifactGroupId="McpRestPaths" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
<moqui.security.ArtifactAuthz userGroupId="MCP_BUSINESS" artifactGroupId="McpScreens" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
<moqui.security.ArtifactAuthz userGroupId="MCP_BUSINESS" artifactGroupId="McpScreenTools" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
<!-- MCP User Accounts -->
......@@ -99,6 +125,7 @@
<!-- Add MCP users to MCP user groups -->
<moqui.security.UserGroupMember userGroupId="McpUser" userId="MCP_USER" fromDate="2025-01-01 00:00:00.000"/>
<moqui.security.UserGroupMember userGroupId="McpUser" userId="JohnSales" fromDate="2025-01-01 00:00:00.000"/>
<moqui.security.UserGroupMember userGroupId="MCP_BUSINESS" userId="MCP_BUSINESS" fromDate="2025-01-01 00:00:00.000"/>
<!-- ADMIN user doesn't need to be in MCP groups - should have full access by default -->
......
......@@ -331,6 +331,22 @@
}
}
// Add screen-based tools
try {
def screenToolsResult = ec.service.sync().name("McpServices.discover#ScreensAsMcpTools")
.parameters([sessionId: sessionId])
.requireNewTransaction(false) // Use current transaction
.disableAuthz()
.call()
if (screenToolsResult?.tools) {
availableTools.addAll(screenToolsResult.tools)
ec.logger.info("MCP ToolsList: Added ${screenToolsResult.tools.size()} screen-based tools")
}
} catch (Exception e) {
ec.logger.warn("Error discovering screen-based tools: ${e.message}")
}
// Implement pagination according to MCP spec
def pageSize = 50 // Reasonable page size for tool lists
def startIndex = 0
......@@ -516,7 +532,7 @@
// Use curated list of commonly used entities instead of discovering all entities
def availableResources = []
ec.logger.info("MCP ResourcesList: Starting permissions-based entity discovery")
ec.logger.info("MCP ResourcesList: Starting permissions-based entity discovery ${userGroups}")
// Get user's accessible entities using Moqui's optimized ArtifactAuthzCheckView
def userAccessibleEntities = null as Set<String>
......@@ -889,6 +905,334 @@
</actions>
</service>
<!-- Screen-based MCP Services -->
<service verb="discover" noun="ScreensAsMcpTools" authenticate="true" allow-remote="true" transaction-timeout="60">
<description>Discover screens accessible to user and convert them to MCP tools</description>
<in-parameters>
<parameter name="sessionId"/>
<parameter name="screenPathPattern" required="false"><description>Optional pattern to filter screen paths (supports wildcards)</description></parameter>
</in-parameters>
<out-parameters>
<parameter name="tools" type="List"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
import org.moqui.impl.context.UserFacadeImpl.UserInfo
import org.moqui.impl.screen.ScreenDefinition
ExecutionContext ec = context.ec
def originalUsername = ec.user.username
def originalUserId = ec.user.userId
def userGroups = ec.user.getUserGroupIdSet().collect { it }
ec.logger.info("MCP Screen Discovery: Starting for user ${originalUsername} (${originalUserId}) with groups ${userGroups}")
def tools = []
// Get user's accessible screens using ArtifactAuthzCheckView
def userAccessibleScreens = null as Set<String>
UserInfo adminUserInfo = null
try {
adminUserInfo = ec.user.pushUser("ADMIN")
def aacvList = ec.entity.find("moqui.security.ArtifactAuthzCheckView")
.condition("userGroupId", userGroups)
.condition("artifactTypeEnumId", "AT_XML_SCREEN")
.useCache(true)
.disableAuthz()
.list()
userAccessibleScreens = aacvList.collect { it.artifactName } as Set<String>
} finally {
if (adminUserInfo != null) {
ec.user.popUser()
}
}
ec.logger.info("MCP Screen Discovery: Found ${userAccessibleScreens.size()} accessible screens")
// Helper function to check if user has permission to a screen
def userHasScreenPermission = { screenPath ->
return userAccessibleScreens != null && userAccessibleScreens.contains(screenPath.toString())
}
// Get all screen definitions and convert accessible ones to MCP tools
try {
adminUserInfo = ec.user.pushUser("ADMIN")
// Get screen locations from component configuration and known screen paths
def screenPaths = []
// Common screen paths to check (using existing Moqui screens)
def commonScreenPaths = [
"apps/ScreenTree",
"apps/AppList",
"webroot/apps",
"webroot/ChangePassword",
"webroot/error",
"webroot/apps/AppList",
"webroot/apps/ScreenTree",
"webroot/apps/ScreenTree/ScreenTreeNested"
]
// Add component-specific screens
def componentScreenLocs = ec.entity.find("moqui.screen.SubscreensItem")
.selectFields(["screenLocation", "subscreenLocation"])
.disableAuthz()
.distinct(true)
.list()
for (compScreen in componentScreenLocs) {
if (compScreen.subscreenLocation) {
screenPaths << compScreen.subscreenLocation
}
}
// Combine all screen paths
screenPaths.addAll(commonScreenPaths)
screenPaths = screenPaths.unique()
// Filter by pattern if provided
if (screenPathPattern) {
def pattern = screenPathPattern.replace("*", ".*")
screenPaths = screenPaths.findAll { it.matches(pattern) }
}
ec.logger.info("MCP Screen Discovery: Checking ${screenPaths.size()} screen paths")
for (screenPath in screenPaths) {
try {
// Check if user has permission to this screen
if (userHasScreenPermission(screenPath)) {
def tool = convertScreenToMcpTool(screenPath, ec)
if (tool) {
tools << tool
}
}
} catch (Exception e) {
ec.logger.debug("Error processing screen ${screenPath}: ${e.message}")
}
}
} finally {
if (adminUserInfo != null) {
ec.user.popUser()
}
}
ec.logger.info("MCP Screen Discovery: Converted ${tools.size()} screens to MCP tools for user ${originalUsername}")
]]></script>
</actions>
</service>
<service verb="convert" noun="ScreenToMcpTool" authenticate="false">
<description>Convert a screen path to MCP tool format</description>
<in-parameters>
<parameter name="screenPath" required="true"/>
</in-parameters>
<out-parameters>
<parameter name="tool" type="Map"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
ExecutionContext ec = context.ec
tool = null
try {
// Try to get screen definition
def screenDef = null
try {
screenDef = ec.screen.getScreenDefinition(screenPath)
} catch (Exception e) {
// Screen might not exist or be accessible
return
}
if (!screenDef) {
return
}
// Extract screen information
def screenName = screenPath.replaceAll("[^a-zA-Z0-9]", "_")
def title = screenDef.getScreenName() ?: screenPath.split("/")[-1]
def description = screenDef.getDescription() ?: "Moqui screen: ${screenPath}"
// Get screen parameters from transitions and forms
def parameters = [:]
def required = []
try {
// Get transitions for parameter discovery
def transitions = screenDef.getTransitionMap()
transitions.each { transitionName, transition ->
transition.getPathParameterList().each { param ->
parameters[param] = [
type: "string",
description: "Path parameter: ${param}"
]
required << param
}
// Get single service parameters if transition calls a service
def serviceName = transition.getSingleServiceName()
if (serviceName) {
try {
def serviceDef = ec.service.getServiceDefinition(serviceName)
if (serviceDef) {
def inParamNames = serviceDef.getInParameterNames()
for (paramName in inParamNames) {
def paramNode = serviceDef.getInParameter(paramName)
def paramType = paramNode?.attribute('type') ?: 'String'
def paramDesc = paramNode.first("description")?.text ?: "Parameter from service ${serviceName}"
// Convert Moqui type to JSON Schema type
def typeMap = [
"text-short": "string",
"text-medium": "string",
"text-long": "string",
"text-very-long": "string",
"id": "string",
"id-long": "string",
"number-integer": "integer",
"number-decimal": "number",
"number-float": "number",
"date": "string",
"date-time": "string",
"date-time-nano": "string",
"boolean": "boolean",
"text-indicator": "boolean"
]
def jsonSchemaType = typeMap[paramType] ?: "string"
parameters[paramName] = [
type: jsonSchemaType,
description: paramDesc
]
if (paramNode?.attribute('required') == "true") {
required << paramName
}
}
}
} catch (Exception e) {
ec.logger.debug("Error getting service definition for ${serviceName}: ${e.message}")
}
}
}
} catch (Exception e) {
ec.logger.debug("Error getting transitions for screen ${screenPath}: ${e.message}")
}
// Build MCP tool
tool = [
name: "screen_${screenName}",
title: title,
description: "${description}. This tool renders the Moqui screen '${screenPath}' and returns the output.",
inputSchema: [
type: "object",
properties: parameters,
required: required.unique()
]
]
// Add screen metadata
tool.screenPath = screenPath
tool.toolType = "screen"
} catch (Exception e) {
ec.logger.warn("Error converting screen ${screenPath} to MCP tool: ${e.message}")
}
]]></script>
</actions>
</service>
<service verb="execute" noun="ScreenAsMcpTool" authenticate="true" allow-remote="true" transaction-timeout="120">
<description>Execute a screen as an MCP tool</description>
<in-parameters>
<parameter name="screenPath" required="true"/>
<parameter name="parameters" type="Map"><description>Parameters to pass to the screen</description></parameter>
<parameter name="renderMode" default="json"><description>Render mode: json, text, csv, xml</description></parameter>
</in-parameters>
<out-parameters>
<parameter name="result" type="Map"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
import groovy.json.JsonBuilder
ExecutionContext ec = context.ec
def startTime = System.currentTimeMillis()
try {
// Validate screen exists
if (!ec.screen.isScreenDefined(screenPath)) {
throw new Exception("Screen not found: ${screenPath}")
}
// Set parameters in context
if (parameters) {
ec.context.putAll(parameters)
}
// Render screen
def screenRender = ec.screen.makeRender()
.screenPath(screenPath)
.renderMode(renderMode)
def output = screenRender.render()
def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
// Convert to MCP format
def content = []
if (output && output.trim().length() > 0) {
content << [
type: "text",
text: output
]
}
result = [
content: content,
isError: false,
metadata: [
screenPath: screenPath,
renderMode: renderMode,
executionTime: executionTime,
outputLength: output?.length() ?: 0
]
]
ec.logger.info("MCP Screen Execution: Successfully executed screen ${screenPath} in ${executionTime}s")
} catch (Exception e) {
def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
result = [
content: [
[
type: "text",
text: "Error executing screen ${screenPath}: ${e.message}"
]
],
isError: true,
metadata: [
screenPath: screenPath,
renderMode: renderMode,
executionTime: executionTime
]
]
ec.logger.error("MCP Screen Execution error for ${screenPath}", e)
}
]]></script>
</actions>
</service>
<!-- NOTE: handle#McpRequest service removed - functionality moved to screen/webapp.xml for unified handling -->
</services>
\ No newline at end of file
......
......@@ -208,6 +208,7 @@ try {
// Look up the actual Visit EntityValue
visit = ec.entity.find("moqui.server.Visit")
.condition("visitId", visitResult.visitId)
.disableAuthz()
.one()
if (!visit) {
throw new Exception("Failed to look up newly created Visit")
......@@ -339,6 +340,7 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}")
// Look up the actual Visit EntityValue
visit = ec.entity.find("moqui.server.Visit")
.condition("visitId", visitResult.visitId)
.disableAuthz()
.one()
if (!visit) {
throw new Exception("Failed to look up newly created Visit")
......@@ -468,6 +470,7 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}")
// Get Visit directly - this is our session
def visit = ec.entity.find("moqui.server.Visit")
.condition("visitId", sessionId)
.disableAuthz()
.one()
if (!visit) {
......@@ -724,6 +727,7 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}")
try {
def existingVisit = ec.entity.find("moqui.server.Visit")
.condition("visitId", sessionId)
.disableAuthz()
.one()
if (!existingVisit) {
......@@ -925,6 +929,7 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}")
// Look up all MCP Visits (persistent)
def mcpVisits = ec.entity.find("moqui.server.Visit")
.condition("initialRequest", "like", "%mcpSession%")
.disableAuthz()
.list()
logger.info("Broadcasting to ${mcpVisits.size()} MCP visits, ${activeConnections.size()} active connections")
......@@ -985,6 +990,7 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}")
// Look up all MCP Visits (persistent)
def mcpVisits = ec.entity.find("moqui.server.Visit")
.condition("initialRequest", "like", "%mcpSession%")
.disableAuthz()
.list()
return [
......