a338d997 by Ean Schuessler

Fix servlet EC lifecycle, remove EntityFind diagnostic, remove renderedContent truncation

- EnhancedMcpServlet: Reuse existing ExecutionContext from MoquiAuthFilter instead of creating duplicate
- EnhancedMcpServlet: Removed manual EC destruction - auth filter handles cleanup
- McpServices: Skip EntityFind objects in serialization (they're query definitions, not data)
- McpServices: Make patchScreenRenderForNullFieldNode static for call from static context
- McpServices: Remove renderedContent for renderMode 'mcp' to avoid JSON duplication/truncation
- AGENTS.md: Document rebuild.sh script usage
- mcp.sh: Use hidden session file (.mcp_session_USER)
- rebuild.sh: Fix shell syntax error and add startup wait logic
1 parent 97d2dc76
......@@ -17,8 +17,26 @@
<#macro "fail-widgets"><#recurse></#macro>
<#-- ================ Subscreens ================ -->
<#macro "subscreens-menu"></#macro>
<#-- ================ Subscreens ================ -->
<#macro "subscreens-menu">
<#if mcpSemanticData??>
<#list sri.getActiveScreenDef().getMenuSubscreensItems() as subscreen>
<#if subscreen.name?has_content>
<#assign urlInstance = sri.buildUrl(subscreen.name)>
<#if urlInstance.isPermitted()>
<#assign fullPath = urlInstance.sui.fullPathNameList![]>
<#assign slashPath = "">
<#list fullPath as pathPart><#assign slashPath = slashPath + (slashPath?has_content)?then("/", "") + pathPart></#list>
<#assign linkText = subscreen.menuTitle?has_content?then(subscreen.menuTitle, subscreen.name)>
<#assign linkType = "navigation">
<#assign dummy = ec.resource.expression("mcpSemanticData.links.add([text: '" + (linkText!"")?js_string + "', path: '" + (slashPath!"")?js_string + "', type: '" + linkType + "'])", "")!>
</#if>
</#if>
</#list>
</#if>
</#macro>
<#macro "subscreens-active">${sri.renderSubscreen()}</#macro>
<#macro "subscreens-panel">${sri.renderSubscreen()}</#macro>
......
......@@ -722,6 +722,10 @@ serializeMoquiObject = { obj, depth = 0, isTerse = false ->
if (obj.getClass().getName().startsWith("org.moqui.impl.screen.ScreenDefinition")) {
return [location: obj.location]
}
// Skip EntityFind objects entirely - they're query definitions, not actual data
if (obj instanceof org.moqui.entity.EntityFind) {
return null
}
// Fallback for unknown types - truncate if too long
def str = obj.toString()
if (str.length() > 200) {
......@@ -1695,11 +1699,6 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
} catch (Exception e) {
ec.logger.warn("BrowseScreens: Failed to generate UI narrative: ${e.message}")
}
// Only truncate if terse=true
if (renderedContent && context.terse == true && renderedContent.length() > 500) {
renderedContent = renderedContent.take(500) + "... (truncated, see uiNarrative for actions)"
}
}
}
......@@ -1713,19 +1712,16 @@ def wikiInstructions = getWikiInstructions(inputScreenPath)
if (actionResult) {
resultMap.actionResult = actionResult
}
// Don't include renderedContent for renderMode "mcp" - semanticState provides structured data
// Including both duplicates data and truncation breaks JSON structure
if (renderedContent && actualRenderMode != "mcp") {
resultMap.renderedContent = renderedContent
}
if (actionError) {
resultMap.actionError = actionError
}
if (renderedContent) {
resultMap.renderedContent = renderedContent
}
// Handle empty content case
if (!renderedContent && !renderError) {
resultMap.renderedContent = "SCREEN_RENDERED_EMPTY: No output generated (screen may be waiting for parameters or has no content)"
}
if (wikiInstructions) {
resultMap.wikiInstructions = wikiInstructions
......
......@@ -32,7 +32,6 @@ import org.slf4j.LoggerFactory
* This provides a proper web context for screen rendering in MCP environment
* using the MCP component's WebFacadeStub instead of the framework's buggy one
*/
@CompileStatic
class CustomScreenTestImpl implements McpScreenTest {
protected final static Logger logger = LoggerFactory.getLogger(CustomScreenTestImpl.class)
......@@ -84,6 +83,46 @@ class CustomScreenTestImpl implements McpScreenTest {
return new org.moqui.mcp.WebFacadeStub(ecfi, parameters, sessionAttributes, requestMethod, screenPath)
}
/**
* Patch ScreenRender instance to handle null fieldNode in getFieldValueString.
* This is a workaround for upstream Moqui bug where widget-template-include
* creates widget nodes with incomplete parent chain.
*/
protected static void patchScreenRenderForNullFieldNode(ScreenRender screenRender) {
try {
def sriImpl = screenRender
def originalGetFieldValueString = sriImpl.&getFieldValueString
sriImpl.metaClass.getFieldValueString = { MNode widgetNode ->
if (widgetNode != null && widgetNode.parent != null && widgetNode.parent.parent != null) {
return originalGetFieldValueString(widgetNode)
} else {
String defaultValue = widgetNode?.attribute("default-value") ?: ""
return delegate.ec.resourceFacade.expandNoL10n(defaultValue, null)
}
}
def originalGetFieldValueClass = sriImpl.&getFieldValueClass
sriImpl.metaClass.getFieldValueClass = { MNode fieldNodeWrapper ->
if (fieldNodeWrapper == null) return "String"
return originalGetFieldValueClass(fieldNodeWrapper)
}
def originalGetFieldEntityValue = sriImpl.&getFieldEntityValue
sriImpl.metaClass.getFieldEntityValue = { MNode widgetNode ->
if (widgetNode != null && widgetNode.parent != null && widgetNode.parent.parent != null) {
return originalGetFieldEntityValue(widgetNode)
} else {
return delegate.getDefaultText(widgetNode)
}
}
logger.debug("Patched ScreenRender with null fieldNode protection")
} catch (Throwable t) {
logger.warn("Failed to patch ScreenRender: ${t.getMessage()}", t)
}
}
@Override
McpScreenTest rootScreen(String screenLocation) {
rootScreenLocation = screenLocation
......@@ -205,7 +244,6 @@ class CustomScreenTestImpl implements McpScreenTest {
/**
* Custom ScreenTestRenderImpl that uses our WebFacadeStub
*/
@CompileStatic
static class CustomScreenTestRenderImpl implements McpScreenTestRender {
protected final CustomScreenTestImpl sti
String screenPath = (String) null
......@@ -334,8 +372,10 @@ class CustomScreenTestImpl implements McpScreenTest {
// Put web facade objects in context for screen access
cs.put("html_scripts", wfs.getHtmlScripts())
cs.put("html_stylesheets", wfs.getHtmlStyleSheets())
// make the ScreenRender
// make ScreenRender
ScreenRender screenRender = csti.sfi.makeRender()
// Patch ScreenRender to handle null fieldNode in getFieldValueString
patchScreenRenderForNullFieldNode(screenRender)
stri.screenRender = screenRender
// pass through various settings
if (csti.rootScreenLocation != null && csti.rootScreenLocation.length() > 0) screenRender.rootScreen(csti.rootScreenLocation)
......
......@@ -127,19 +127,17 @@ class EnhancedMcpServlet extends HttpServlet {
if (handleCors(request, response, webappName, ecfi)) return
long startTime = System.currentTimeMillis()
if (logger.traceEnabled) {
logger.trace("Start Enhanced MCP request to [${request.getPathInfo()}] at time [${startTime}] in session [${request.session.id}] thread [${Thread.currentThread().id}:${Thread.currentThread().name}]")
}
ExecutionContextImpl activeEc = ecfi.activeContext.get()
if (activeEc != null) {
logger.warn("In EnhancedMcpServlet.service there is already an ExecutionContext for user ${activeEc.user.username}")
activeEc.destroy()
ExecutionContextImpl ec = ecfi.activeContext.get()
if (ec == null) {
logger.warn("No ExecutionContext found from MoquiAuthFilter, creating new one")
ec = ecfi.getEci()
}
ExecutionContextImpl ec = ecfi.getEci()
try {
// Read request body VERY early before any other processing can consume it
String requestBody = null
......@@ -235,8 +233,6 @@ class EnhancedMcpServlet extends HttpServlet {
// Use simple JSON string to avoid Groovy JSON library issues
def errorMsg = t.message?.toString() ?: "Unknown error"
response.writer.write("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32603,\"message\":\"Internal error: ${errorMsg.replace("\"", "\\\"")}\"},\"id\":null}")
} finally {
ec.destroy()
}
}
......