Skip to content
Toggle navigation
Toggle navigation
This project
Loading...
Sign in
Ean Schuessler
/
mo-mcp
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Commits
Issue Boards
Files
Commits
Network
Compare
Branches
Tags
f4695781
authored
2025-11-20 22:32:22 -0600
by
Ean Schuessler
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
start adding screen resource support
1 parent
1d9ca524
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
378 additions
and
1 deletions
data/McpSecuritySeedData.xml
service/McpServices.xml
src/main/groovy/org/moqui/mcp/EnhancedMcpServlet.groovy
data/McpSecuritySeedData.xml
View file @
f469578
...
...
@@ -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 -->
...
...
service/McpServices.xml
View file @
f469578
...
...
@@ -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
...
...
src/main/groovy/org/moqui/mcp/EnhancedMcpServlet.groovy
View file @
f469578
...
...
@@ -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
[
...
...
Please
register
or
sign in
to post a comment