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 @@ ...@@ -24,4 +24,13 @@
24 </webapp> 24 </webapp>
25 </webapp-list> 25 </webapp-list>
26 26
27 <screen-facade>
28 <screen-text-output type="mcp" mime-type="text/markdown" always-standalone="true"
29 macro-template-location="component://moqui-mcp-2/screen/macro/DefaultScreenMacros.mcp.ftl"/>
30 <screen-text-output type="json" mime-type="application/json" always-standalone="true"
31 macro-template-location="component://moqui-mcp-2/screen/macro/DefaultScreenMacros.json.ftl"/>
32 <widget-render-mode type="mcp" widget-render-class="org.moqui.impl.screen.ScreenWidgetRenderFtl"/>
33 <widget-render-mode type="json" widget-render-class="org.moqui.impl.screen.ScreenWidgetRenderFtl"/>
34 </screen-facade>
35
27 </moqui-conf> 36 </moqui-conf>
...\ No newline at end of file ...\ No newline at end of file
......
1 <#--
2 This software is in the public domain under CC0 1.0 Universal plus a Grant of Patent License.
3
4 To the extent possible under law, the author(s) have dedicated all
5 copyright and related and neighboring rights to this software to the
6 public domain worldwide. This software is distributed without any
7 warranty.
8
9 You should have received a copy of the CC0 Public Domain Dedication
10 along with this software (see the LICENSE.md file). If not, see
11 <http://creativecommons.org/publicdomain/zero/1.0/>.
12 -->
13 <#-- NOTE: no empty lines before the first #macro otherwise FTL outputs empty lines -->
14 <#-- ==================== Includes ==================== -->
15 <#macro "include-screen">${sri.renderIncludeScreen(.node["@location"], .node["@share-scope"]!)}</#macro>
16
17 <#-- ============== Render Mode Elements ============== -->
18 <#macro "render-mode">
19 <#if .node["text"]?has_content>
20 <#list .node["text"] as textNode><#if !textNode["@type"]?has_content || textNode["@type"] == "any"><#local textToUse = textNode/></#if></#list>
21 <#list .node["text"] as textNode><#if textNode["@type"]?has_content && textNode["@type"]?split(",")?seq_contains(sri.getRenderMode())><#local textToUse = textNode></#if></#list>
22 <#if textToUse??><@renderText textNode=textToUse/></#if>
23 </#if>
24 </#macro>
25 <#macro text>
26 <#if !.node["@type"]?has_content || (.node["@type"]?split(",")?seq_contains(sri.getRenderMode()))><@renderText textNode=.node/></#if>
27 </#macro>
28 <#macro renderText textNode>
29 <#if textNode["@location"]?has_content>
30 <#assign textLocation = ec.getResource().expandNoL10n(textNode["@location"], "")>
31 <#if sri.doBoundaryComments() && textNode["@no-boundary-comment"]! != "true">
32 <!-- BEGIN render-mode.text[@location=${textLocation}][@template=${textNode["@template"]!"true"}] -->
33 </#if>
34 <#-- NOTE: this still won't encode templates that are rendered to the writer -->
35 <#t><#if .node["@encode"]! == "true">${sri.renderText(textLocation, textNode["@template"]!)?html}<#else>${sri.renderText(textLocation, textNode["@template"]!)}</#if>
36 <#if sri.doBoundaryComments() && textNode["@no-boundary-comment"]! != "true"><!-- END render-mode.text[@location=${textLocation}][@template=${textNode["@template"]!"true"}] --></#if>
37 </#if>
38 <#assign inlineTemplateSource = textNode.@@text!>
39 <#if inlineTemplateSource?has_content>
40 <#if sri.doBoundaryComments() && textNode["@no-boundary-comment"]! != "true"><!-- BEGIN render-mode.text[inline][@template=${textNode["@template"]!"true"}] --></#if>
41 <#if !textNode["@template"]?has_content || textNode["@template"] == "true">
42 <#assign inlineTemplate = [inlineTemplateSource, sri.getActiveScreenDef().location + ".render_mode.text"]?interpret>
43 <@inlineTemplate/>
44 <#else>
45 <#if .node["@encode"]! == "true">${inlineTemplateSource?html}<#else>${inlineTemplateSource}</#if>
46 </#if>
47 <#if sri.doBoundaryComments() && textNode["@no-boundary-comment"]! != "true"><!-- END render-mode.text[inline][@template=${textNode["@template"]!"true"}] --></#if>
48 </#if>
49 </#macro>
50
1 <#--
2 Moqui JSON Optimized Macros
3 Renders screens in structured JSON format.
4 -->
5
6 <#include "DefaultScreenMacros.any.ftl"/>
7
8 <#macro @element></#macro>
9
10 <#macro screen>{"screen": {<#recurse>}}</#macro>
11
12 <#macro widgets>
13 "widgets": [<#recurse>]
14 </#macro>
15
16 <#macro "fail-widgets"><#recurse></#macro>
17
18 <#-- ================ Subscreens ================ -->
19 <#macro "subscreens-menu"></#macro>
20 <#macro "subscreens-active">{"type": "subscreens-active", "content": ${sri.renderSubscreen()}}</#macro>
21 <#macro "subscreens-panel">{"type": "subscreens-panel", "content": ${sri.renderSubscreen()}}</#macro>
22
23 <#-- ================ Section ================ -->
24 <#macro section>{"type": "section", "name": "${.node["@name"]}", "content": ${sri.renderSection(.node["@name"])}}</#macro>
25
26 <#-- ================ Containers ================ -->
27 <#macro container>
28 {"type": "container", "id": "${.node["@id"]!""}", "style": "${.node["@style"]!""}", "children": [<#recurse>]}
29 </#macro>
30
31 <#macro label>
32 {"type": "label", "text": "${ec.resource.expand(.node["@text"], "")?json_string}"}
33 </#macro>
34
35 <#macro link>
36 {"type": "link", "text": "${ec.resource.expand(.node["@text"]!"", "")?json_string}", "url": "${.node["@url"]!""}"}
37 </#macro>
1 <#--
2 Moqui MCP Optimized Macros
3 Renders screens in Markdown format optimized for LLM consumption.
4 -->
5
6 <#include "DefaultScreenMacros.any.ftl"/>
7
8 <#macro @element></#macro>
9
10 <#macro screen><#recurse></#macro>
11
12 <#macro widgets>
13 <#recurse>
14 </#macro>
15
16 <#macro "fail-widgets"><#recurse></#macro>
17
18 <#-- ================ Subscreens ================ -->
19 <#macro "subscreens-menu"></#macro>
20 <#macro "subscreens-active">${sri.renderSubscreen()}</#macro>
21 <#macro "subscreens-panel">${sri.renderSubscreen()}</#macro>
22
23 <#-- ================ Section ================ -->
24 <#macro section>${sri.renderSection(.node["@name"])}</#macro>
25 <#macro "section-iterate">${sri.renderSection(.node["@name"])}</#macro>
26 <#macro "section-include">${sri.renderSectionInclude(.node)}</#macro>
27
28 <#-- ================ Containers ================ -->
29 <#macro container>
30 <#recurse>
31 </#macro>
32
33 <#macro "container-box">
34 <#if .node["box-header"]?has_content>### <#recurse .node["box-header"][0]></#if>
35 <#if .node["box-body"]?has_content><#recurse .node["box-body"][0]></#if>
36 <#if .node["box-body-nopad"]?has_content><#recurse .node["box-body-nopad"][0]></#if>
37 </#macro>
38
39 <#macro "container-row"><#list .node["row-col"] as rowColNode><#recurse rowColNode></#list></#macro>
40
41 <#macro "container-panel">
42 <#if .node["panel-header"]?has_content>### <#recurse .node["panel-header"][0]></#if>
43 <#if .node["panel-left"]?has_content><#recurse .node["panel-left"][0]></#if>
44 <#recurse .node["panel-center"][0]>
45 <#if .node["panel-right"]?has_content><#recurse .node["panel-right"][0]></#if>
46 <#if .node["panel-footer"]?has_content><#recurse .node["panel-footer"][0]></#if>
47 </#macro>
48
49 <#macro "container-dialog">
50 [Button: ${ec.resource.expand(.node["@button-text"], "")}]
51 </#macro>
52
53 <#-- ================== Standalone Fields ==================== -->
54 <#macro link>
55 <#assign linkNode = .node>
56 <#if linkNode["@condition"]?has_content><#assign conditionResult = ec.getResource().condition(linkNode["@condition"], "")><#else><#assign conditionResult = true></#if>
57 <#if conditionResult>
58 <#assign urlInstance = sri.makeUrlByType(linkNode["@url"]!"", linkNode["@url-type"]!"transition", linkNode, "true")>
59 <#assign linkText = "">
60 <#if linkNode["@text"]?has_content>
61 <#assign linkText = ec.getResource().expand(linkNode["@text"], "")>
62 <#elseif linkNode["@entity-name"]?has_content>
63 <#assign linkText = sri.getFieldEntityValue(linkNode)>
64 </#if>
65 <#if !linkText?has_content && .node?parent?node_name?ends_with("-field")>
66 <#assign linkText = sri.getFieldValueString(.node?parent?parent)>
67 </#if>
68
69 <#-- Convert path to dot notation for moqui_render_screen -->
70 <#assign fullPath = urlInstance.sui.fullPathNameList![]>
71 <#assign dotPath = "">
72 <#list fullPath as pathPart><#assign dotPath = dotPath + (dotPath?has_content)?then(".", "") + pathPart></#list>
73
74 <#assign paramStr = urlInstance.getParameterString()>
75 <#if paramStr?has_content><#assign dotPath = dotPath + "?" + paramStr></#if>
76
77 [${linkText}](${dotPath})<#t>
78 </#if>
79 </#macro>
80
81 <#macro image>![${.node["@alt"]!""}](${(.node["@url"]!"")})</#macro>
82
83 <#macro label>
84 <#assign text = ec.resource.expand(.node["@text"], "")>
85 <#assign type = .node["@type"]!"span">
86 <#if type == "h1"># ${text}
87 <#elseif type == "h2">## ${text}
88 <#elseif type == "h3">### ${text}
89 <#elseif type == "p">${text}
90 <#else>${text}</#if>
91 </#macro>
92
93 <#-- ======================= Form ========================= -->
94 <#macro "form-single">
95 <#assign formNode = sri.getFormNode(.node["@name"])>
96 <#assign mapName = formNode["@map"]!"fieldValues">
97 <#t>${sri.pushSingleFormMapContext(mapName)}
98 <#list formNode["field"] as fieldNode>
99 <#assign fieldSubNode = "">
100 <#list fieldNode["conditional-field"] as csf><#if ec.resource.condition(csf["@condition"], "")><#assign fieldSubNode = csf><#break></#if></#list>
101 <#if !fieldSubNode?has_content><#assign fieldSubNode = fieldNode["default-field"][0]!></#if>
102 <#if fieldSubNode?has_content && !fieldSubNode["ignored"]?has_content && !fieldSubNode["hidden"]?has_content && !fieldSubNode["submit"]?has_content && fieldSubNode?parent["@hide"]! != "true">
103 <#assign title><@fieldTitle fieldSubNode/></#assign>
104 * **${title}**: <#recurse fieldSubNode>
105 </#if>
106 </#list>
107 <#t>${sri.popContext()}
108 </#macro>
109
110 <#macro "form-list">
111 <#assign formInstance = sri.getFormInstance(.node["@name"])>
112 <#assign formListInfo = formInstance.makeFormListRenderInfo()>
113 <#assign formNode = formListInfo.getFormNode()>
114 <#assign formListColumnList = formListInfo.getAllColInfo()>
115 <#assign listObject = formListInfo.getListObject(false)!>
116
117 <#-- Header Row -->
118 <#list formListColumnList as columnFieldList>
119 <#assign fieldNode = columnFieldList[0]>
120 <#assign fieldSubNode = fieldNode["header-field"][0]!fieldNode["default-field"][0]!fieldNode["conditional-field"][0]!>
121 <#t>| <@fieldTitle fieldSubNode/><#t>
122 </#list>
123 |
124 <#list formListColumnList as columnFieldList>| --- </#list>|
125 <#-- Data Rows -->
126 <#list listObject as listEntry>
127 <#t>${sri.startFormListRow(formListInfo, listEntry, listEntry_index, listEntry_has_next)}
128 <#list formListColumnList as columnFieldList>
129 <#t>| <#list columnFieldList as fieldNode><@formListSubField fieldNode/><#if fieldNode_has_next> </#if></#list><#t>
130 </#list>
131 |
132 <#t>${sri.endFormListRow()}
133 </#list>
134 <#t>${sri.safeCloseList(listObject)}
135 </#macro>
136
137 <#macro formListSubField fieldNode>
138 <#list fieldNode["conditional-field"] as fieldSubNode>
139 <#if ec.resource.condition(fieldSubNode["@condition"], "")>
140 <#t><@formListWidget fieldSubNode/>
141 <#return>
142 </#if>
143 </#list>
144 <#if fieldNode["default-field"]?has_content>
145 <#t><@formListWidget fieldNode["default-field"][0]/>
146 </#if>
147 </#macro>
148
149 <#macro formListWidget fieldSubNode>
150 <#if fieldSubNode["ignored"]?has_content || fieldSubNode["hidden"]?has_content || fieldSubNode["submit"]?has_content || fieldSubNode?parent["@hide"]! == "true"><#return></#if>
151 <#recurse fieldSubNode>
152 </#macro>
153
154 <#macro fieldTitle fieldSubNode>
155 <#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>
156 <#t>${ec.l10n.localize(titleValue)}
157 </#macro>
158
159 <#-- ================== Form Field Widgets ==================== -->
160 <#macro "check">
161 <#assign options = sri.getFieldOptions(.node)!>
162 <#assign currentValue = sri.getFieldValueString(.node)>
163 <#t>${(options.get(currentValue))!(currentValue)}
164 </#macro>
165
166 <#macro "date-find"></#macro>
167 <#macro "date-time">
168 <#assign javaFormat = .node["@format"]!>
169 <#if !javaFormat?has_content>
170 <#if .node["@type"]! == "time"><#assign javaFormat="HH:mm">
171 <#elseif .node["@type"]! == "date"><#assign javaFormat="yyyy-MM-dd">
172 <#else><#assign javaFormat="yyyy-MM-dd HH:mm"></#if>
173 </#if>
174 <#assign fieldValue = sri.getFieldValueString(.node?parent?parent, .node["@default-value"]!"", javaFormat)>
175 <#t>${fieldValue}
176 </#macro>
177
178 <#macro "display">
179 <#assign fieldValue = "">
180 <#assign dispFieldNode = .node?parent?parent>
181 <#if .node["@text"]?has_content>
182 <#assign textMap = {}>
183 <#if .node["@text-map"]?has_content><#assign textMap = ec.getResource().expression(.node["@text-map"], {})!></#if>
184 <#assign fieldValue = ec.getResource().expand(.node["@text"], "", textMap, false)>
185 <#if .node["@currency-unit-field"]?has_content>
186 <#assign fieldValue = ec.getL10n().formatCurrency(fieldValue, ec.getResource().expression(.node["@currency-unit-field"], ""))>
187 </#if>
188 <#else>
189 <#assign fieldValue = sri.getFieldValueString(.node)>
190 </#if>
191 <#t>${fieldValue}
192 </#macro>
193
194 <#macro "display-entity">
195 <#t>${sri.getFieldEntityValue(.node)}
196 </#macro>
197
198 <#macro "drop-down">
199 <#assign options = sri.getFieldOptions(.node)>
200 <#assign currentValue = sri.getFieldValueString(.node)>
201 <#t>${(options.get(currentValue))!(currentValue)}
202 </#macro>
203
204 <#macro "text-area"><#t>${sri.getFieldValueString(.node)}</#macro>
205 <#macro "text-line"><#t>${sri.getFieldValueString(.node)}</#macro>
206 <#macro "text-find"><#t>${sri.getFieldValueString(.node)}</#macro>
207 <#macro "submit"></#macro>
208 <#macro "password"></#macro>
209 <#macro "hidden"></#macro>
...@@ -142,7 +142,7 @@ ...@@ -142,7 +142,7 @@
142 if (name == "moqui_render_screen") { 142 if (name == "moqui_render_screen") {
143 def screenPath = arguments?.path 143 def screenPath = arguments?.path
144 def parameters = arguments?.parameters ?: [:] 144 def parameters = arguments?.parameters ?: [:]
145 def renderMode = arguments?.renderMode ?: "html" 145 def renderMode = arguments?.renderMode ?: "mcp"
146 def subscreenName = arguments?.subscreenName 146 def subscreenName = arguments?.subscreenName
147 147
148 if (!screenPath) throw new Exception("moqui_render_screen requires 'path' parameter") 148 if (!screenPath) throw new Exception("moqui_render_screen requires 'path' parameter")
...@@ -257,7 +257,7 @@ ...@@ -257,7 +257,7 @@
257 timestamp: System.currentTimeMillis() 257 timestamp: System.currentTimeMillis()
258 ] 258 ]
259 ] 259 ]
260 servlet.queueNotification(sessionId, notification) 260 //servlet.queueNotification(sessionId, notification)
261 } 261 }
262 } catch (Exception e) { 262 } catch (Exception e) {
263 ec.logger.warn("Failed to send tool execution notification: ${e.message}") 263 ec.logger.warn("Failed to send tool execution notification: ${e.message}")
...@@ -368,13 +368,24 @@ ...@@ -368,13 +368,24 @@
368 // Query entity data 368 // Query entity data
369 def entityList = ec.entity.find(entityName).limit(100).list() 369 def entityList = ec.entity.find(entityName).limit(100).list()
370 370
371 // Format response 371 // Format response for MCP - create multiple content objects
372 def contentList = []
373
374 // Add main content with entity data as text
375 contentList << [
376 type: "text",
377 text: new JsonBuilder([
378 entityName: entityName,
379 description: entityDef.description,
380 packageName: entityDef.packageName,
381 recordCount: entityList.size(),
382 data: entityList
383 ]).toString()
384 ]
385
372 def responseMap = [ 386 def responseMap = [
373 entityName: entityName, 387 content: contentList,
374 description: entityDef.description, 388 isError: false
375 packageName: entityDef.packageName,
376 recordCount: entityList.size(),
377 data: entityList
378 ] 389 ]
379 390
380 def jsonOutput = new JsonBuilder(responseMap).toString() 391 def jsonOutput = new JsonBuilder(responseMap).toString()
...@@ -596,7 +607,7 @@ ...@@ -596,7 +607,7 @@
596 <in-parameters> 607 <in-parameters>
597 <parameter name="screenPath" required="true"/> 608 <parameter name="screenPath" required="true"/>
598 <parameter name="parameters" type="Map"><description>Parameters to pass to the screen</description></parameter> 609 <parameter name="parameters" type="Map"><description>Parameters to pass to the screen</description></parameter>
599 <parameter name="renderMode" default="html"><description>Render mode: text, html, xml, vuet, qvt</description></parameter> 610 <parameter name="renderMode" default="mcp"><description>Render mode: mcp, text, html, xml, vuet, qvt</description></parameter>
600 <parameter name="sessionId"><description>Session ID for user context restoration</description></parameter> 611 <parameter name="sessionId"><description>Session ID for user context restoration</description></parameter>
601 <parameter name="subscreenName"><description>Optional subscreen name for dot notation paths</description></parameter> 612 <parameter name="subscreenName"><description>Optional subscreen name for dot notation paths</description></parameter>
602 </in-parameters> 613 </in-parameters>
...@@ -704,7 +715,7 @@ def startTime = System.currentTimeMillis() ...@@ -704,7 +715,7 @@ def startTime = System.currentTimeMillis()
704 // Regular screen rendering with current user context - use our custom ScreenTestImpl 715 // Regular screen rendering with current user context - use our custom ScreenTestImpl
705 def screenTest = new org.moqui.mcp.CustomScreenTestImpl(ec.ecfi) 716 def screenTest = new org.moqui.mcp.CustomScreenTestImpl(ec.ecfi)
706 .rootScreen(rootScreen) 717 .rootScreen(rootScreen)
707 .renderMode(renderMode ? renderMode : "html") 718 .renderMode(renderMode ? renderMode : "mcp")
708 .auth(ec.user.username) 719 .auth(ec.user.username)
709 720
710 def renderParams = parameters ?: [:] 721 def renderParams = parameters ?: [:]
...@@ -1254,7 +1265,7 @@ def startTime = System.currentTimeMillis() ...@@ -1254,7 +1265,7 @@ def startTime = System.currentTimeMillis()
1254 properties: [ 1265 properties: [
1255 path: [type: "string", description: "Screen path (e.g. 'PopCommerce.Catalog.Product')"], 1266 path: [type: "string", description: "Screen path (e.g. 'PopCommerce.Catalog.Product')"],
1256 parameters: [type: "object", description: "Parameters for the screen"], 1267 parameters: [type: "object", description: "Parameters for the screen"],
1257 renderMode: [type: "string", description: "html, text, or json", default: "html"] 1268 renderMode: [type: "string", description: "mcp, text, html, xml, vuet, qvt", default: "mcp"]
1258 ], 1269 ],
1259 required: ["path"] 1270 required: ["path"]
1260 ] 1271 ]
......
...@@ -295,6 +295,10 @@ class CustomScreenTestImpl implements McpScreenTest { ...@@ -295,6 +295,10 @@ class CustomScreenTestImpl implements McpScreenTest {
295 org.moqui.mcp.WebFacadeStub wfs = (org.moqui.mcp.WebFacadeStub) csti.createWebFacade(csti.ecfi, stri.parameters, csti.sessionAttributes, stri.requestMethod, stri.screenPath) 295 org.moqui.mcp.WebFacadeStub wfs = (org.moqui.mcp.WebFacadeStub) csti.createWebFacade(csti.ecfi, stri.parameters, csti.sessionAttributes, stri.requestMethod, stri.screenPath)
296 // set stub on eci, will also put parameters in the context 296 // set stub on eci, will also put parameters in the context
297 eci.setWebFacade(wfs) 297 eci.setWebFacade(wfs)
298
299 // Put web facade objects in context for screen access
300 cs.put("html_scripts", wfs.getHtmlScripts())
301 cs.put("html_stylesheets", wfs.getHtmlStyleSheets())
298 // make the ScreenRender 302 // make the ScreenRender
299 ScreenRender screenRender = csti.sfi.makeRender() 303 ScreenRender screenRender = csti.sfi.makeRender()
300 stri.screenRender = screenRender 304 stri.screenRender = screenRender
......
...@@ -228,7 +228,7 @@ class EnhancedMcpServlet extends HttpServlet { ...@@ -228,7 +228,7 @@ class EnhancedMcpServlet extends HttpServlet {
228 if ("GET".equals(method) && requestURI.endsWith("/sse")) { 228 if ("GET".equals(method) && requestURI.endsWith("/sse")) {
229 handleSseConnection(request, response, ec, webappName) 229 handleSseConnection(request, response, ec, webappName)
230 } else if ("POST".equals(method) && requestURI.endsWith("/message")) { 230 } else if ("POST".equals(method) && requestURI.endsWith("/message")) {
231 handleMessage(request, response, ec) 231 handleMessage(request, response, ec, requestBody)
232 } else if ("POST".equals(method) && (requestURI.equals("/mcp") || requestURI.endsWith("/mcp"))) { 232 } else if ("POST".equals(method) && (requestURI.equals("/mcp") || requestURI.endsWith("/mcp"))) {
233 // Handle POST requests to /mcp for JSON-RPC 233 // Handle POST requests to /mcp for JSON-RPC
234 logger.info("About to call handleJsonRpc with visit: ${visit?.visitId}") 234 logger.info("About to call handleJsonRpc with visit: ${visit?.visitId}")
...@@ -245,11 +245,8 @@ class EnhancedMcpServlet extends HttpServlet { ...@@ -245,11 +245,8 @@ class EnhancedMcpServlet extends HttpServlet {
245 logger.warn("Enhanced MCP Access Forbidden (no authz): " + e.message) 245 logger.warn("Enhanced MCP Access Forbidden (no authz): " + e.message)
246 response.setStatus(HttpServletResponse.SC_FORBIDDEN) 246 response.setStatus(HttpServletResponse.SC_FORBIDDEN)
247 response.setContentType("application/json") 247 response.setContentType("application/json")
248 response.writer.write(JsonOutput.toJson([ 248 def msg = e.message?.toString() ?: "Access forbidden"
249 jsonrpc: "2.0", 249 response.writer.write("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32001,\"message\":\"Access Forbidden: ${msg.replace("\"", "\\\"")}\"},\"id\":null}")
250 error: [code: -32001, message: "Access Forbidden: " + e.message],
251 id: null
252 ]))
253 } catch (ArtifactTarpitException e) { 250 } catch (ArtifactTarpitException e) {
254 logger.warn("Enhanced MCP Too Many Requests (tarpit): " + e.message) 251 logger.warn("Enhanced MCP Too Many Requests (tarpit): " + e.message)
255 response.setStatus(429) 252 response.setStatus(429)
...@@ -266,11 +263,9 @@ class EnhancedMcpServlet extends HttpServlet { ...@@ -266,11 +263,9 @@ class EnhancedMcpServlet extends HttpServlet {
266 logger.error("Error in Enhanced MCP request", t) 263 logger.error("Error in Enhanced MCP request", t)
267 response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR) 264 response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
268 response.setContentType("application/json") 265 response.setContentType("application/json")
269 response.writer.write(JsonOutput.toJson([ 266 // Use simple JSON string to avoid Groovy JSON library issues
270 jsonrpc: "2.0", 267 def errorMsg = t.message?.toString() ?: "Unknown error"
271 error: [code: -32603, message: "Internal error: " + t.message], 268 response.writer.write("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32603,\"message\":\"Internal error: ${errorMsg.replace("\"", "\\\"")}\"},\"id\":null}")
272 id: null
273 ]))
274 } finally { 269 } finally {
275 ec.destroy() 270 ec.destroy()
276 } 271 }
...@@ -337,6 +332,7 @@ class EnhancedMcpServlet extends HttpServlet { ...@@ -337,6 +332,7 @@ class EnhancedMcpServlet extends HttpServlet {
337 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "System configuration error: Web facade failed to initialize. Check Moqui logs for details.") 332 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "System configuration error: Web facade failed to initialize. Check Moqui logs for details.")
338 return 333 return
339 } 334 }
335 }
340 336
341 // Final check that we have a Visit 337 // Final check that we have a Visit
342 if (!visit) { 338 if (!visit) {
...@@ -433,10 +429,20 @@ class EnhancedMcpServlet extends HttpServlet { ...@@ -433,10 +429,20 @@ class EnhancedMcpServlet extends HttpServlet {
433 request.getAsyncContext().complete() 429 request.getAsyncContext().complete()
434 } catch (Exception e) { 430 } catch (Exception e) {
435 logger.debug("Error completing async context: ${e.message}") 431 logger.debug("Error completing async context: ${e.message}")
436 }
437 } 432 }
438 } 433 }
439 } 434 }
435 }
436
437 private void handleMessage(HttpServletRequest request, HttpServletResponse response, ExecutionContextImpl ec, String requestBody)
438 throws IOException {
439
440 String sessionId = request.getHeader("Mcp-Session-Id")
441 def visit = getCachedVisit(ec, sessionId)
442 if (!visit) {
443 response.sendError(HttpServletResponse.SC_NOT_FOUND, "Session not found: " + sessionId)
444 return
445 }
440 446
441 // Verify user has access to this Visit - rely on Moqui security 447 // Verify user has access to this Visit - rely on Moqui security
442 logger.info("Session validation: visit.userId=${visit.userId}, ec.user.userId=${ec.user.userId}, ec.user.username=${ec.user.username}") 448 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 { ...@@ -456,29 +462,7 @@ class EnhancedMcpServlet extends HttpServlet {
456 VisitBasedMcpSession session = new VisitBasedMcpSession(visit, response.writer, ec) 462 VisitBasedMcpSession session = new VisitBasedMcpSession(visit, response.writer, ec)
457 463
458 try { 464 try {
459 // Read request body 465 if (!requestBody || !requestBody.trim()) {
460 StringBuilder body = new StringBuilder()
461 try {
462 BufferedReader reader = request.getReader()
463 String line
464 while ((line = reader.readLine()) != null) {
465 body.append(line)
466 }
467 } catch (IOException e) {
468 logger.error("Failed to read request body: ${e.message}")
469 response.setContentType("application/json")
470 response.setCharacterEncoding("UTF-8")
471 response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
472 response.writer.write(JsonOutput.toJson([
473 jsonrpc: "2.0",
474 error: [code: -32700, message: "Failed to read request body: " + e.message],
475 id: null
476 ]))
477 return
478 }
479
480 String requestBody = body.toString()
481 if (!requestBody.trim()) {
482 response.setContentType("application/json") 466 response.setContentType("application/json")
483 response.setCharacterEncoding("UTF-8") 467 response.setCharacterEncoding("UTF-8")
484 response.setStatus(HttpServletResponse.SC_BAD_REQUEST) 468 response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
...@@ -572,9 +556,8 @@ class EnhancedMcpServlet extends HttpServlet { ...@@ -572,9 +556,8 @@ class EnhancedMcpServlet extends HttpServlet {
572 556
573 String method = request.getMethod() 557 String method = request.getMethod()
574 String acceptHeader = request.getHeader("Accept") 558 String acceptHeader = request.getHeader("Accept")
575 String contentType = request.getContentType()
576 559
577 logger.info("Enhanced MCP JSON-RPC Request: ${method} ${request.requestURI} - Accept: ${acceptHeader}, Content-Type: ${contentType}") 560 logger.info("Enhanced MCP JSON-RPC Request: ${method} ${request.requestURI} - Accept: ${acceptHeader}")
578 561
579 // Validate Accept header per MCP 2025-11-25 spec requirement #2 562 // Validate Accept header per MCP 2025-11-25 spec requirement #2
580 // Client MUST include Accept header listing both application/json and text/event-stream 563 // Client MUST include Accept header listing both application/json and text/event-stream
...@@ -583,7 +566,7 @@ class EnhancedMcpServlet extends HttpServlet { ...@@ -583,7 +566,7 @@ class EnhancedMcpServlet extends HttpServlet {
583 response.setContentType("application/json") 566 response.setContentType("application/json")
584 response.writer.write(JsonOutput.toJson([ 567 response.writer.write(JsonOutput.toJson([
585 jsonrpc: "2.0", 568 jsonrpc: "2.0",
586 error: [code: -32600, message: "Accept header must include both application/json and text/event-stream per MCP 2025-11-25 spec"], 569 error: [code: -32600, message: "Accept header must include application/json and text/event-stream"],
587 id: null 570 id: null
588 ])) 571 ]))
589 return 572 return
...@@ -594,28 +577,7 @@ class EnhancedMcpServlet extends HttpServlet { ...@@ -594,28 +577,7 @@ class EnhancedMcpServlet extends HttpServlet {
594 response.setContentType("application/json") 577 response.setContentType("application/json")
595 response.writer.write(JsonOutput.toJson([ 578 response.writer.write(JsonOutput.toJson([
596 jsonrpc: "2.0", 579 jsonrpc: "2.0",
597 error: [code: -32601, message: "Method Not Allowed. Use POST for JSON-RPC or GET /mcp-sse/sse for SSE."], 580 error: [code: -32601, message: "Method Not Allowed. Use POST for JSON-RPC."],
598 id: null
599 ]))
600 return
601 }
602
603 // Use pre-read request body
604 logger.info("Using pre-read request body, length: ${requestBody?.length()}")
605
606 String jsonMethod = request.getMethod()
607 String jsonAcceptHeader = request.getHeader("Accept")
608 String jsonContentType = request.getContentType()
609
610 logger.info("Enhanced MCP JSON-RPC Request: ${jsonMethod} ${request.requestURI} - Accept: ${jsonAcceptHeader}, Content-Type: ${jsonContentType}")
611
612 // Handle POST requests for JSON-RPC
613 if (!"POST".equals(jsonMethod)) {
614 response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED)
615 response.setContentType("application/json")
616 response.writer.write(JsonOutput.toJson([
617 jsonrpc: "2.0",
618 error: [code: -32601, message: "Method Not Allowed. Use POST for JSON-RPC or GET /mcp-sse/sse for SSE."],
619 id: null 581 id: null
620 ])) 582 ]))
621 return 583 return
...@@ -774,6 +736,7 @@ class EnhancedMcpServlet extends HttpServlet { ...@@ -774,6 +736,7 @@ class EnhancedMcpServlet extends HttpServlet {
774 if (sessionId) { 736 if (sessionId) {
775 response.setHeader("Mcp-Session-Id", sessionId.toString()) 737 response.setHeader("Mcp-Session-Id", sessionId.toString())
776 } 738 }
739 response.setContentType("text/event-stream")
777 response.setStatus(HttpServletResponse.SC_ACCEPTED) // 202 Accepted 740 response.setStatus(HttpServletResponse.SC_ACCEPTED) // 202 Accepted
778 logger.info("Sent 202 Accepted response for notifications/initialized") 741 logger.info("Sent 202 Accepted response for notifications/initialized")
779 response.flushBuffer() // Commit the response immediately 742 response.flushBuffer() // Commit the response immediately
...@@ -817,11 +780,6 @@ class EnhancedMcpServlet extends HttpServlet { ...@@ -817,11 +780,6 @@ class EnhancedMcpServlet extends HttpServlet {
817 logger.info("Set Mcp-Session-Id header to ${responseSessionId} for method ${rpcRequest.method}") 780 logger.info("Set Mcp-Session-Id header to ${responseSessionId} for method ${rpcRequest.method}")
818 } 781 }
819 782
820 if (responseSessionId) {
821 response.setHeader("Mcp-Session-Id", responseSessionId)
822 logger.info("Set Mcp-Session-Id header to ${responseSessionId} for method ${rpcRequest.method}")
823 }
824
825 // Build JSON-RPC response for regular requests 783 // Build JSON-RPC response for regular requests
826 // Extract the actual result from Moqui service response 784 // Extract the actual result from Moqui service response
827 def actualResult = result?.result ?: result 785 def actualResult = result?.result ?: result
...@@ -841,9 +799,8 @@ class EnhancedMcpServlet extends HttpServlet { ...@@ -841,9 +799,8 @@ class EnhancedMcpServlet extends HttpServlet {
841 def notificationContent = [] 799 def notificationContent = []
842 for (notification in pendingNotifications) { 800 for (notification in pendingNotifications) {
843 notificationContent << [ 801 notificationContent << [
844 type: "notification", 802 type: "text",
845 text: JsonOutput.toJson(notification.params ?: notification), 803 text: "Notification [${notification.method}]: " + JsonOutput.toJson(notification.params ?: notification)
846 method: notification.method
847 ] 804 ]
848 } 805 }
849 806
...@@ -1151,7 +1108,7 @@ class EnhancedMcpServlet extends HttpServlet { ...@@ -1151,7 +1108,7 @@ class EnhancedMcpServlet extends HttpServlet {
1151 method: notification.method ?: "notifications/message", 1108 method: notification.method ?: "notifications/message",
1152 params: notification.params ?: notification 1109 params: notification.params ?: notification
1153 ] 1110 ]
1154 sendSseEvent(writer, "notification", JsonOutput.toJson(notificationMessage), System.currentTimeMillis()) 1111 sendSseEvent(writer, "message", JsonOutput.toJson(notificationMessage), System.currentTimeMillis())
1155 logger.info("Sent notification via SSE to session ${sessionId}") 1112 logger.info("Sent notification via SSE to session ${sessionId}")
1156 } catch (Exception e) { 1113 } catch (Exception e) {
1157 logger.warn("Failed to send notification via SSE to session ${sessionId}: ${e.message}") 1114 logger.warn("Failed to send notification via SSE to session ${sessionId}: ${e.message}")
...@@ -1270,7 +1227,7 @@ class EnhancedMcpServlet extends HttpServlet { ...@@ -1270,7 +1227,7 @@ class EnhancedMcpServlet extends HttpServlet {
1270 PrintWriter writer = activeConnections.get(visit.visitId) 1227 PrintWriter writer = activeConnections.get(visit.visitId)
1271 if (writer && !writer.checkError()) { 1228 if (writer && !writer.checkError()) {
1272 try { 1229 try {
1273 sendSseEvent(writer, "broadcast", message.toJson()) 1230 sendSseEvent(writer, "message", message.toJson())
1274 successCount++ 1231 successCount++
1275 } catch (Exception e) { 1232 } catch (Exception e) {
1276 logger.warn("Failed to send broadcast to ${visit.visitId}: ${e.message}") 1233 logger.warn("Failed to send broadcast to ${visit.visitId}: ${e.message}")
......
...@@ -54,6 +54,10 @@ class WebFacadeStub implements WebFacade { ...@@ -54,6 +54,10 @@ class WebFacadeStub implements WebFacade {
54 54
55 protected List<Map> screenHistory = [] 55 protected List<Map> screenHistory = []
56 56
57 // Web context objects needed by screens
58 protected Set<String> html_scripts = new LinkedHashSet<>()
59 protected Set<String> html_stylesheets = new LinkedHashSet<>()
60
57 protected String responseText = null 61 protected String responseText = null
58 protected Object responseJsonObj = null 62 protected Object responseJsonObj = null
59 boolean skipJsonSerialize = false 63 boolean skipJsonSerialize = false
...@@ -221,6 +225,10 @@ class WebFacadeStub implements WebFacade { ...@@ -221,6 +225,10 @@ class WebFacadeStub implements WebFacade {
221 @Override 225 @Override
222 List<Map> getScreenHistory() { return screenHistory } 226 List<Map> getScreenHistory() { return screenHistory }
223 227
228 List<String> getHtmlScripts() { return new ArrayList<>(html_scripts) }
229
230 List<String> getHtmlStyleSheets() { return new ArrayList<>(html_stylesheets) }
231
224 @Override 232 @Override
225 void sendJsonResponse(Object responseObj) { 233 void sendJsonResponse(Object responseObj) {
226 if (!skipJsonSerialize) { 234 if (!skipJsonSerialize) {
......