185763c0 by Ean Schuessler

Update MCP services, servlet, and screen macros for enhanced markdown rendering and spec compliance

1 parent 9f389520
......@@ -24,4 +24,13 @@
</webapp>
</webapp-list>
<screen-facade>
<screen-text-output type="mcp" mime-type="text/markdown" always-standalone="true"
macro-template-location="component://moqui-mcp-2/screen/macro/DefaultScreenMacros.mcp.ftl"/>
<screen-text-output type="json" mime-type="application/json" always-standalone="true"
macro-template-location="component://moqui-mcp-2/screen/macro/DefaultScreenMacros.json.ftl"/>
<widget-render-mode type="mcp" widget-render-class="org.moqui.impl.screen.ScreenWidgetRenderFtl"/>
<widget-render-mode type="json" widget-render-class="org.moqui.impl.screen.ScreenWidgetRenderFtl"/>
</screen-facade>
</moqui-conf>
\ No newline at end of file
......
<#--
This software is in the public domain under CC0 1.0 Universal plus a Grant of Patent License.
To the extent possible under law, the 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/>.
-->
<#-- NOTE: no empty lines before the first #macro otherwise FTL outputs empty lines -->
<#-- ==================== Includes ==================== -->
<#macro "include-screen">${sri.renderIncludeScreen(.node["@location"], .node["@share-scope"]!)}</#macro>
<#-- ============== Render Mode Elements ============== -->
<#macro "render-mode">
<#if .node["text"]?has_content>
<#list .node["text"] as textNode><#if !textNode["@type"]?has_content || textNode["@type"] == "any"><#local textToUse = textNode/></#if></#list>
<#list .node["text"] as textNode><#if textNode["@type"]?has_content && textNode["@type"]?split(",")?seq_contains(sri.getRenderMode())><#local textToUse = textNode></#if></#list>
<#if textToUse??><@renderText textNode=textToUse/></#if>
</#if>
</#macro>
<#macro text>
<#if !.node["@type"]?has_content || (.node["@type"]?split(",")?seq_contains(sri.getRenderMode()))><@renderText textNode=.node/></#if>
</#macro>
<#macro renderText textNode>
<#if textNode["@location"]?has_content>
<#assign textLocation = ec.getResource().expandNoL10n(textNode["@location"], "")>
<#if sri.doBoundaryComments() && textNode["@no-boundary-comment"]! != "true">
<!-- BEGIN render-mode.text[@location=${textLocation}][@template=${textNode["@template"]!"true"}] -->
</#if>
<#-- NOTE: this still won't encode templates that are rendered to the writer -->
<#t><#if .node["@encode"]! == "true">${sri.renderText(textLocation, textNode["@template"]!)?html}<#else>${sri.renderText(textLocation, textNode["@template"]!)}</#if>
<#if sri.doBoundaryComments() && textNode["@no-boundary-comment"]! != "true"><!-- END render-mode.text[@location=${textLocation}][@template=${textNode["@template"]!"true"}] --></#if>
</#if>
<#assign inlineTemplateSource = textNode.@@text!>
<#if inlineTemplateSource?has_content>
<#if sri.doBoundaryComments() && textNode["@no-boundary-comment"]! != "true"><!-- BEGIN render-mode.text[inline][@template=${textNode["@template"]!"true"}] --></#if>
<#if !textNode["@template"]?has_content || textNode["@template"] == "true">
<#assign inlineTemplate = [inlineTemplateSource, sri.getActiveScreenDef().location + ".render_mode.text"]?interpret>
<@inlineTemplate/>
<#else>
<#if .node["@encode"]! == "true">${inlineTemplateSource?html}<#else>${inlineTemplateSource}</#if>
</#if>
<#if sri.doBoundaryComments() && textNode["@no-boundary-comment"]! != "true"><!-- END render-mode.text[inline][@template=${textNode["@template"]!"true"}] --></#if>
</#if>
</#macro>
<#--
Moqui JSON Optimized Macros
Renders screens in structured JSON format.
-->
<#include "DefaultScreenMacros.any.ftl"/>
<#macro @element></#macro>
<#macro screen>{"screen": {<#recurse>}}</#macro>
<#macro widgets>
"widgets": [<#recurse>]
</#macro>
<#macro "fail-widgets"><#recurse></#macro>
<#-- ================ Subscreens ================ -->
<#macro "subscreens-menu"></#macro>
<#macro "subscreens-active">{"type": "subscreens-active", "content": ${sri.renderSubscreen()}}</#macro>
<#macro "subscreens-panel">{"type": "subscreens-panel", "content": ${sri.renderSubscreen()}}</#macro>
<#-- ================ Section ================ -->
<#macro section>{"type": "section", "name": "${.node["@name"]}", "content": ${sri.renderSection(.node["@name"])}}</#macro>
<#-- ================ Containers ================ -->
<#macro container>
{"type": "container", "id": "${.node["@id"]!""}", "style": "${.node["@style"]!""}", "children": [<#recurse>]}
</#macro>
<#macro label>
{"type": "label", "text": "${ec.resource.expand(.node["@text"], "")?json_string}"}
</#macro>
<#macro link>
{"type": "link", "text": "${ec.resource.expand(.node["@text"]!"", "")?json_string}", "url": "${.node["@url"]!""}"}
</#macro>
<#--
Moqui MCP Optimized Macros
Renders screens in Markdown format optimized for LLM consumption.
-->
<#include "DefaultScreenMacros.any.ftl"/>
<#macro @element></#macro>
<#macro screen><#recurse></#macro>
<#macro widgets>
<#recurse>
</#macro>
<#macro "fail-widgets"><#recurse></#macro>
<#-- ================ Subscreens ================ -->
<#macro "subscreens-menu"></#macro>
<#macro "subscreens-active">${sri.renderSubscreen()}</#macro>
<#macro "subscreens-panel">${sri.renderSubscreen()}</#macro>
<#-- ================ Section ================ -->
<#macro section>${sri.renderSection(.node["@name"])}</#macro>
<#macro "section-iterate">${sri.renderSection(.node["@name"])}</#macro>
<#macro "section-include">${sri.renderSectionInclude(.node)}</#macro>
<#-- ================ Containers ================ -->
<#macro container>
<#recurse>
</#macro>
<#macro "container-box">
<#if .node["box-header"]?has_content>### <#recurse .node["box-header"][0]></#if>
<#if .node["box-body"]?has_content><#recurse .node["box-body"][0]></#if>
<#if .node["box-body-nopad"]?has_content><#recurse .node["box-body-nopad"][0]></#if>
</#macro>
<#macro "container-row"><#list .node["row-col"] as rowColNode><#recurse rowColNode></#list></#macro>
<#macro "container-panel">
<#if .node["panel-header"]?has_content>### <#recurse .node["panel-header"][0]></#if>
<#if .node["panel-left"]?has_content><#recurse .node["panel-left"][0]></#if>
<#recurse .node["panel-center"][0]>
<#if .node["panel-right"]?has_content><#recurse .node["panel-right"][0]></#if>
<#if .node["panel-footer"]?has_content><#recurse .node["panel-footer"][0]></#if>
</#macro>
<#macro "container-dialog">
[Button: ${ec.resource.expand(.node["@button-text"], "")}]
</#macro>
<#-- ================== Standalone Fields ==================== -->
<#macro link>
<#assign linkNode = .node>
<#if linkNode["@condition"]?has_content><#assign conditionResult = ec.getResource().condition(linkNode["@condition"], "")><#else><#assign conditionResult = true></#if>
<#if conditionResult>
<#assign urlInstance = sri.makeUrlByType(linkNode["@url"]!"", linkNode["@url-type"]!"transition", linkNode, "true")>
<#assign linkText = "">
<#if linkNode["@text"]?has_content>
<#assign linkText = ec.getResource().expand(linkNode["@text"], "")>
<#elseif linkNode["@entity-name"]?has_content>
<#assign linkText = sri.getFieldEntityValue(linkNode)>
</#if>
<#if !linkText?has_content && .node?parent?node_name?ends_with("-field")>
<#assign linkText = sri.getFieldValueString(.node?parent?parent)>
</#if>
<#-- Convert path to dot notation for moqui_render_screen -->
<#assign fullPath = urlInstance.sui.fullPathNameList![]>
<#assign dotPath = "">
<#list fullPath as pathPart><#assign dotPath = dotPath + (dotPath?has_content)?then(".", "") + pathPart></#list>
<#assign paramStr = urlInstance.getParameterString()>
<#if paramStr?has_content><#assign dotPath = dotPath + "?" + paramStr></#if>
[${linkText}](${dotPath})<#t>
</#if>
</#macro>
<#macro image>![${.node["@alt"]!""}](${(.node["@url"]!"")})</#macro>
<#macro label>
<#assign text = ec.resource.expand(.node["@text"], "")>
<#assign type = .node["@type"]!"span">
<#if type == "h1"># ${text}
<#elseif type == "h2">## ${text}
<#elseif type == "h3">### ${text}
<#elseif type == "p">${text}
<#else>${text}</#if>
</#macro>
<#-- ======================= Form ========================= -->
<#macro "form-single">
<#assign formNode = sri.getFormNode(.node["@name"])>
<#assign mapName = formNode["@map"]!"fieldValues">
<#t>${sri.pushSingleFormMapContext(mapName)}
<#list formNode["field"] as fieldNode>
<#assign fieldSubNode = "">
<#list fieldNode["conditional-field"] as csf><#if ec.resource.condition(csf["@condition"], "")><#assign fieldSubNode = csf><#break></#if></#list>
<#if !fieldSubNode?has_content><#assign fieldSubNode = fieldNode["default-field"][0]!></#if>
<#if fieldSubNode?has_content && !fieldSubNode["ignored"]?has_content && !fieldSubNode["hidden"]?has_content && !fieldSubNode["submit"]?has_content && fieldSubNode?parent["@hide"]! != "true">
<#assign title><@fieldTitle fieldSubNode/></#assign>
* **${title}**: <#recurse fieldSubNode>
</#if>
</#list>
<#t>${sri.popContext()}
</#macro>
<#macro "form-list">
<#assign formInstance = sri.getFormInstance(.node["@name"])>
<#assign formListInfo = formInstance.makeFormListRenderInfo()>
<#assign formNode = formListInfo.getFormNode()>
<#assign formListColumnList = formListInfo.getAllColInfo()>
<#assign listObject = formListInfo.getListObject(false)!>
<#-- Header Row -->
<#list formListColumnList as columnFieldList>
<#assign fieldNode = columnFieldList[0]>
<#assign fieldSubNode = fieldNode["header-field"][0]!fieldNode["default-field"][0]!fieldNode["conditional-field"][0]!>
<#t>| <@fieldTitle fieldSubNode/><#t>
</#list>
|
<#list formListColumnList as columnFieldList>| --- </#list>|
<#-- Data Rows -->
<#list listObject as listEntry>
<#t>${sri.startFormListRow(formListInfo, listEntry, listEntry_index, listEntry_has_next)}
<#list formListColumnList as columnFieldList>
<#t>| <#list columnFieldList as fieldNode><@formListSubField fieldNode/><#if fieldNode_has_next> </#if></#list><#t>
</#list>
|
<#t>${sri.endFormListRow()}
</#list>
<#t>${sri.safeCloseList(listObject)}
</#macro>
<#macro formListSubField fieldNode>
<#list fieldNode["conditional-field"] as fieldSubNode>
<#if ec.resource.condition(fieldSubNode["@condition"], "")>
<#t><@formListWidget fieldSubNode/>
<#return>
</#if>
</#list>
<#if fieldNode["default-field"]?has_content>
<#t><@formListWidget fieldNode["default-field"][0]/>
</#if>
</#macro>
<#macro formListWidget fieldSubNode>
<#if fieldSubNode["ignored"]?has_content || fieldSubNode["hidden"]?has_content || fieldSubNode["submit"]?has_content || fieldSubNode?parent["@hide"]! == "true"><#return></#if>
<#recurse fieldSubNode>
</#macro>
<#macro fieldTitle fieldSubNode>
<#assign titleValue><#if fieldSubNode["@title"]?has_content>${fieldSubNode["@title"]}<#else><#list fieldSubNode?parent["@name"]?split("(?=[A-Z])", "r") as nameWord>${nameWord?cap_first?replace("Id", "ID")}<#if nameWord_has_next> </#if></#list></#if></#assign>
<#t>${ec.l10n.localize(titleValue)}
</#macro>
<#-- ================== Form Field Widgets ==================== -->
<#macro "check">
<#assign options = sri.getFieldOptions(.node)!>
<#assign currentValue = sri.getFieldValueString(.node)>
<#t>${(options.get(currentValue))!(currentValue)}
</#macro>
<#macro "date-find"></#macro>
<#macro "date-time">
<#assign javaFormat = .node["@format"]!>
<#if !javaFormat?has_content>
<#if .node["@type"]! == "time"><#assign javaFormat="HH:mm">
<#elseif .node["@type"]! == "date"><#assign javaFormat="yyyy-MM-dd">
<#else><#assign javaFormat="yyyy-MM-dd HH:mm"></#if>
</#if>
<#assign fieldValue = sri.getFieldValueString(.node?parent?parent, .node["@default-value"]!"", javaFormat)>
<#t>${fieldValue}
</#macro>
<#macro "display">
<#assign fieldValue = "">
<#assign dispFieldNode = .node?parent?parent>
<#if .node["@text"]?has_content>
<#assign textMap = {}>
<#if .node["@text-map"]?has_content><#assign textMap = ec.getResource().expression(.node["@text-map"], {})!></#if>
<#assign fieldValue = ec.getResource().expand(.node["@text"], "", textMap, false)>
<#if .node["@currency-unit-field"]?has_content>
<#assign fieldValue = ec.getL10n().formatCurrency(fieldValue, ec.getResource().expression(.node["@currency-unit-field"], ""))>
</#if>
<#else>
<#assign fieldValue = sri.getFieldValueString(.node)>
</#if>
<#t>${fieldValue}
</#macro>
<#macro "display-entity">
<#t>${sri.getFieldEntityValue(.node)}
</#macro>
<#macro "drop-down">
<#assign options = sri.getFieldOptions(.node)>
<#assign currentValue = sri.getFieldValueString(.node)>
<#t>${(options.get(currentValue))!(currentValue)}
</#macro>
<#macro "text-area"><#t>${sri.getFieldValueString(.node)}</#macro>
<#macro "text-line"><#t>${sri.getFieldValueString(.node)}</#macro>
<#macro "text-find"><#t>${sri.getFieldValueString(.node)}</#macro>
<#macro "submit"></#macro>
<#macro "password"></#macro>
<#macro "hidden"></#macro>
......@@ -142,7 +142,7 @@
if (name == "moqui_render_screen") {
def screenPath = arguments?.path
def parameters = arguments?.parameters ?: [:]
def renderMode = arguments?.renderMode ?: "html"
def renderMode = arguments?.renderMode ?: "mcp"
def subscreenName = arguments?.subscreenName
if (!screenPath) throw new Exception("moqui_render_screen requires 'path' parameter")
......@@ -257,7 +257,7 @@
timestamp: System.currentTimeMillis()
]
]
servlet.queueNotification(sessionId, notification)
//servlet.queueNotification(sessionId, notification)
}
} catch (Exception e) {
ec.logger.warn("Failed to send tool execution notification: ${e.message}")
......@@ -368,13 +368,24 @@
// Query entity data
def entityList = ec.entity.find(entityName).limit(100).list()
// Format response
def responseMap = [
// Format response for MCP - create multiple content objects
def contentList = []
// Add main content with entity data as text
contentList << [
type: "text",
text: new JsonBuilder([
entityName: entityName,
description: entityDef.description,
packageName: entityDef.packageName,
recordCount: entityList.size(),
data: entityList
]).toString()
]
def responseMap = [
content: contentList,
isError: false
]
def jsonOutput = new JsonBuilder(responseMap).toString()
......@@ -596,7 +607,7 @@
<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="html"><description>Render mode: text, html, xml, vuet, qvt</description></parameter>
<parameter name="renderMode" default="mcp"><description>Render mode: mcp, text, html, xml, vuet, qvt</description></parameter>
<parameter name="sessionId"><description>Session ID for user context restoration</description></parameter>
<parameter name="subscreenName"><description>Optional subscreen name for dot notation paths</description></parameter>
</in-parameters>
......@@ -704,7 +715,7 @@ def startTime = System.currentTimeMillis()
// Regular screen rendering with current user context - use our custom ScreenTestImpl
def screenTest = new org.moqui.mcp.CustomScreenTestImpl(ec.ecfi)
.rootScreen(rootScreen)
.renderMode(renderMode ? renderMode : "html")
.renderMode(renderMode ? renderMode : "mcp")
.auth(ec.user.username)
def renderParams = parameters ?: [:]
......@@ -1254,7 +1265,7 @@ def startTime = System.currentTimeMillis()
properties: [
path: [type: "string", description: "Screen path (e.g. 'PopCommerce.Catalog.Product')"],
parameters: [type: "object", description: "Parameters for the screen"],
renderMode: [type: "string", description: "html, text, or json", default: "html"]
renderMode: [type: "string", description: "mcp, text, html, xml, vuet, qvt", default: "mcp"]
],
required: ["path"]
]
......
......@@ -295,6 +295,10 @@ class CustomScreenTestImpl implements McpScreenTest {
org.moqui.mcp.WebFacadeStub wfs = (org.moqui.mcp.WebFacadeStub) csti.createWebFacade(csti.ecfi, stri.parameters, csti.sessionAttributes, stri.requestMethod, stri.screenPath)
// set stub on eci, will also put parameters in the context
eci.setWebFacade(wfs)
// Put web facade objects in context for screen access
cs.put("html_scripts", wfs.getHtmlScripts())
cs.put("html_stylesheets", wfs.getHtmlStyleSheets())
// make the ScreenRender
ScreenRender screenRender = csti.sfi.makeRender()
stri.screenRender = screenRender
......
......@@ -228,7 +228,7 @@ class EnhancedMcpServlet extends HttpServlet {
if ("GET".equals(method) && requestURI.endsWith("/sse")) {
handleSseConnection(request, response, ec, webappName)
} else if ("POST".equals(method) && requestURI.endsWith("/message")) {
handleMessage(request, response, ec)
handleMessage(request, response, ec, requestBody)
} else if ("POST".equals(method) && (requestURI.equals("/mcp") || requestURI.endsWith("/mcp"))) {
// Handle POST requests to /mcp for JSON-RPC
logger.info("About to call handleJsonRpc with visit: ${visit?.visitId}")
......@@ -245,11 +245,8 @@ class EnhancedMcpServlet extends HttpServlet {
logger.warn("Enhanced MCP Access Forbidden (no authz): " + e.message)
response.setStatus(HttpServletResponse.SC_FORBIDDEN)
response.setContentType("application/json")
response.writer.write(JsonOutput.toJson([
jsonrpc: "2.0",
error: [code: -32001, message: "Access Forbidden: " + e.message],
id: null
]))
def msg = e.message?.toString() ?: "Access forbidden"
response.writer.write("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32001,\"message\":\"Access Forbidden: ${msg.replace("\"", "\\\"")}\"},\"id\":null}")
} catch (ArtifactTarpitException e) {
logger.warn("Enhanced MCP Too Many Requests (tarpit): " + e.message)
response.setStatus(429)
......@@ -266,11 +263,9 @@ class EnhancedMcpServlet extends HttpServlet {
logger.error("Error in Enhanced MCP request", t)
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
response.setContentType("application/json")
response.writer.write(JsonOutput.toJson([
jsonrpc: "2.0",
error: [code: -32603, message: "Internal error: " + t.message],
id: null
]))
// 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()
}
......@@ -337,6 +332,7 @@ class EnhancedMcpServlet extends HttpServlet {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "System configuration error: Web facade failed to initialize. Check Moqui logs for details.")
return
}
}
// Final check that we have a Visit
if (!visit) {
......@@ -436,7 +432,17 @@ class EnhancedMcpServlet extends HttpServlet {
}
}
}
}
}
private void handleMessage(HttpServletRequest request, HttpServletResponse response, ExecutionContextImpl ec, String requestBody)
throws IOException {
String sessionId = request.getHeader("Mcp-Session-Id")
def visit = getCachedVisit(ec, sessionId)
if (!visit) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Session not found: " + sessionId)
return
}
// Verify user has access to this Visit - rely on Moqui security
logger.info("Session validation: visit.userId=${visit.userId}, ec.user.userId=${ec.user.userId}, ec.user.username=${ec.user.username}")
......@@ -456,29 +462,7 @@ class EnhancedMcpServlet extends HttpServlet {
VisitBasedMcpSession session = new VisitBasedMcpSession(visit, response.writer, ec)
try {
// Read request body
StringBuilder body = new StringBuilder()
try {
BufferedReader reader = request.getReader()
String line
while ((line = reader.readLine()) != null) {
body.append(line)
}
} catch (IOException e) {
logger.error("Failed to read request body: ${e.message}")
response.setContentType("application/json")
response.setCharacterEncoding("UTF-8")
response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
response.writer.write(JsonOutput.toJson([
jsonrpc: "2.0",
error: [code: -32700, message: "Failed to read request body: " + e.message],
id: null
]))
return
}
String requestBody = body.toString()
if (!requestBody.trim()) {
if (!requestBody || !requestBody.trim()) {
response.setContentType("application/json")
response.setCharacterEncoding("UTF-8")
response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
......@@ -572,9 +556,8 @@ class EnhancedMcpServlet extends HttpServlet {
String method = request.getMethod()
String acceptHeader = request.getHeader("Accept")
String contentType = request.getContentType()
logger.info("Enhanced MCP JSON-RPC Request: ${method} ${request.requestURI} - Accept: ${acceptHeader}, Content-Type: ${contentType}")
logger.info("Enhanced MCP JSON-RPC Request: ${method} ${request.requestURI} - Accept: ${acceptHeader}")
// Validate Accept header per MCP 2025-11-25 spec requirement #2
// Client MUST include Accept header listing both application/json and text/event-stream
......@@ -583,7 +566,7 @@ class EnhancedMcpServlet extends HttpServlet {
response.setContentType("application/json")
response.writer.write(JsonOutput.toJson([
jsonrpc: "2.0",
error: [code: -32600, message: "Accept header must include both application/json and text/event-stream per MCP 2025-11-25 spec"],
error: [code: -32600, message: "Accept header must include application/json and text/event-stream"],
id: null
]))
return
......@@ -594,28 +577,7 @@ class EnhancedMcpServlet extends HttpServlet {
response.setContentType("application/json")
response.writer.write(JsonOutput.toJson([
jsonrpc: "2.0",
error: [code: -32601, message: "Method Not Allowed. Use POST for JSON-RPC or GET /mcp-sse/sse for SSE."],
id: null
]))
return
}
// Use pre-read request body
logger.info("Using pre-read request body, length: ${requestBody?.length()}")
String jsonMethod = request.getMethod()
String jsonAcceptHeader = request.getHeader("Accept")
String jsonContentType = request.getContentType()
logger.info("Enhanced MCP JSON-RPC Request: ${jsonMethod} ${request.requestURI} - Accept: ${jsonAcceptHeader}, Content-Type: ${jsonContentType}")
// Handle POST requests for JSON-RPC
if (!"POST".equals(jsonMethod)) {
response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED)
response.setContentType("application/json")
response.writer.write(JsonOutput.toJson([
jsonrpc: "2.0",
error: [code: -32601, message: "Method Not Allowed. Use POST for JSON-RPC or GET /mcp-sse/sse for SSE."],
error: [code: -32601, message: "Method Not Allowed. Use POST for JSON-RPC."],
id: null
]))
return
......@@ -774,6 +736,7 @@ class EnhancedMcpServlet extends HttpServlet {
if (sessionId) {
response.setHeader("Mcp-Session-Id", sessionId.toString())
}
response.setContentType("text/event-stream")
response.setStatus(HttpServletResponse.SC_ACCEPTED) // 202 Accepted
logger.info("Sent 202 Accepted response for notifications/initialized")
response.flushBuffer() // Commit the response immediately
......@@ -817,11 +780,6 @@ class EnhancedMcpServlet extends HttpServlet {
logger.info("Set Mcp-Session-Id header to ${responseSessionId} for method ${rpcRequest.method}")
}
if (responseSessionId) {
response.setHeader("Mcp-Session-Id", responseSessionId)
logger.info("Set Mcp-Session-Id header to ${responseSessionId} for method ${rpcRequest.method}")
}
// Build JSON-RPC response for regular requests
// Extract the actual result from Moqui service response
def actualResult = result?.result ?: result
......@@ -841,9 +799,8 @@ class EnhancedMcpServlet extends HttpServlet {
def notificationContent = []
for (notification in pendingNotifications) {
notificationContent << [
type: "notification",
text: JsonOutput.toJson(notification.params ?: notification),
method: notification.method
type: "text",
text: "Notification [${notification.method}]: " + JsonOutput.toJson(notification.params ?: notification)
]
}
......@@ -1151,7 +1108,7 @@ class EnhancedMcpServlet extends HttpServlet {
method: notification.method ?: "notifications/message",
params: notification.params ?: notification
]
sendSseEvent(writer, "notification", JsonOutput.toJson(notificationMessage), System.currentTimeMillis())
sendSseEvent(writer, "message", JsonOutput.toJson(notificationMessage), System.currentTimeMillis())
logger.info("Sent notification via SSE to session ${sessionId}")
} catch (Exception e) {
logger.warn("Failed to send notification via SSE to session ${sessionId}: ${e.message}")
......@@ -1270,7 +1227,7 @@ class EnhancedMcpServlet extends HttpServlet {
PrintWriter writer = activeConnections.get(visit.visitId)
if (writer && !writer.checkError()) {
try {
sendSseEvent(writer, "broadcast", message.toJson())
sendSseEvent(writer, "message", message.toJson())
successCount++
} catch (Exception e) {
logger.warn("Failed to send broadcast to ${visit.visitId}: ${e.message}")
......
......@@ -54,6 +54,10 @@ class WebFacadeStub implements WebFacade {
protected List<Map> screenHistory = []
// Web context objects needed by screens
protected Set<String> html_scripts = new LinkedHashSet<>()
protected Set<String> html_stylesheets = new LinkedHashSet<>()
protected String responseText = null
protected Object responseJsonObj = null
boolean skipJsonSerialize = false
......@@ -221,6 +225,10 @@ class WebFacadeStub implements WebFacade {
@Override
List<Map> getScreenHistory() { return screenHistory }
List<String> getHtmlScripts() { return new ArrayList<>(html_scripts) }
List<String> getHtmlStyleSheets() { return new ArrayList<>(html_stylesheets) }
@Override
void sendJsonResponse(Object responseObj) {
if (!skipJsonSerialize) {
......