e41ccca9 by Ean Schuessler

WIP: Add SDK-based MCP servlet implementations with SSE support

- Add multiple servlet implementations (EnhancedMcpServlet, ServiceBasedMcpServlet, MoquiMcpServlet)
- Implement SSE servlet support with proper content-type handling
- Add MCP filter for request processing
- Add web.xml configuration for servlet deployment
- Include SDK framework JAR and configuration files
- Remove old screen-based MCP implementation
- Update component configuration for new servlet-based approach
1 parent c65f866b
No preview for this file type
No preview for this file type
No preview for this file type
File mode changed
#Fri Nov 14 19:52:12 CST 2025
gradle.version=8.9
arguments=--init-script /home/ean/.config/Code/User/globalStorage/redhat.java/1.46.0/config_linux/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle --init-script /home/ean/.config/Code/User/globalStorage/redhat.java/1.46.0/config_linux/org.eclipse.osgi/58/0/.cp/gradle/protobuf/init.gradle
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.9))
connection.project.dir=../../..
eclipse.preferences.version=1
gradle.user.home=
java.home=/usr/lib/jvm/java-17-openjdk-amd64
jvm.arguments=
offline.mode=false
override.workspace.settings=true
show.console.view=true
show.executions.view=true
/*
* 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/>.
*/
apply plugin: 'groovy'
def componentNode = parseComponent(project)
version = componentNode.'@version'
def jarBaseName = componentNode.'@name'
def moquiDir = projectDir.parentFile.parentFile.parentFile
def frameworkDir = file(moquiDir.absolutePath + '/framework')
repositories {
flatDir name: 'localLib', dirs: frameworkDir.absolutePath + '/lib'
mavenCentral()
}
// Log4J has annotation processors, disable to avoid warning
tasks.withType(JavaCompile) { options.compilerArgs << "-proc:none" }
tasks.withType(GroovyCompile) { options.compilerArgs << "-proc:none" }
dependencies {
implementation project(':framework')
// Servlet API (provided by framework, but needed for compilation)
compileOnly 'javax.servlet:javax.servlet-api:4.0.1'
}
// by default the Java plugin runs test on build, change to not do that (only run test if explicit task)
check.dependsOn.clear()
task cleanLib(type: Delete) { delete fileTree(dir: projectDir.absolutePath+'/lib', include: '*') }
clean.dependsOn cleanLib
jar {
destinationDirectory = file(projectDir.absolutePath + '/lib')
archiveBaseName = jarBaseName
}
task copyDependencies { doLast {
copy { from (configurations.runtimeClasspath - project(':framework').configurations.runtimeClasspath)
into file(projectDir.absolutePath + '/lib') }
} }
copyDependencies.dependsOn cleanLib
jar.dependsOn copyDependencies
\ No newline at end of file
......@@ -14,13 +14,49 @@
xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/component-definition-3.xsd"
name="moqui-mcp-2">
<!-- No dependencies - uses only core framework -->
<!-- MCP SDK dependencies -->
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>0.16.0</version>
</dependency>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-core</artifactId>
<version>0.16.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.15.2</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.5.10</version>
</dependency>
<entity-factory load-path="entity/" />
<service-factory load-path="service/" />
<screen-factory load-path="screen/" />
<!-- <screen-factory load-path="screen/" /> -->
<!-- Load seed data -->
<entity-factory load-data="data/McpSecuritySeedData.xml" />
<!-- Register MCP filter
<webapp-list>
<webapp name="webroot">
<filter name="McpFilter" class="org.moqui.mcp.McpFilter">
<url-pattern>/mcpservlet/*</url-pattern>
</filter>
</webapp>
</webapp-list> -->
</component>
......
No preview for this file type
......@@ -257,10 +257,14 @@
def userId = ec.user.userId
def userAccountId = userId ? userId : null
// Build server capabilities
// Get user-specific tools and resources
def toolsResult = ec.service.sync().name("org.moqui.mcp.McpServices.mcp#ToolsList").parameters([:]).call()
def resourcesResult = ec.service.sync().name("org.moqui.mcp.McpServices.mcp#ResourcesList").parameters([:]).call()
// Build server capabilities based on what user can access
def serverCapabilities = [
tools: [:],
resources: [:],
tools: toolsResult?.result?.tools ? [listChanged: true] : [:],
resources: resourcesResult?.result?.resources ? [subscribe: true, listChanged: true] : [:],
logging: [:]
]
......@@ -274,8 +278,10 @@
protocolVersion: "2025-06-18",
capabilities: serverCapabilities,
serverInfo: serverInfo,
instructions: "This server provides access to Moqui ERP services and entities through MCP. Use tools/list to discover available operations."
instructions: "This server provides access to Moqui ERP services and entities through MCP. Tools and resources are filtered based on your permissions."
]
ec.logger.info("MCP Initialize for user ${userId}: ${toolsResult?.result?.tools?.size() ?: 0} tools, ${resourcesResult?.result?.resources?.size() ?: 0} resources")
]]></script>
</actions>
</service>
......@@ -321,13 +327,27 @@
]
]
// Add service metadata to help LLM
if (serviceInfo.verb && serviceInfo.noun) {
tool.description += " (${serviceInfo.verb}:${serviceInfo.noun})"
}
// Convert service parameters to JSON Schema
def inParamNames = serviceInfo.getInParameterNames()
for (paramName in inParamNames) {
def paramInfo = serviceInfo.getInParameter(paramName)
def paramDesc = paramInfo.description ?: ""
// Add type information to description for LLM
if (!paramDesc) {
paramDesc = "Parameter of type ${paramInfo.type}"
} else {
paramDesc += " (type: ${paramInfo.type})"
}
tool.inputSchema.properties[paramName] = [
type: convertMoquiTypeToJsonSchemaType(paramInfo.type),
description: paramInfo.description ?: ""
description: paramDesc
]
if (paramInfo.required) {
......@@ -474,10 +494,15 @@
def resource = [
uri: "entity://${entityName}",
name: entityName,
description: "Moqui entity: ${entityName}",
description: entityInfo.description ?: "Moqui entity: ${entityName}",
mimeType: "application/json"
]
// Add entity metadata to help LLM
if (entityInfo.packageName) {
resource.description += " (package: ${entityInfo.packageName})"
}
availableResources << resource
} catch (Exception e) {
......@@ -536,6 +561,9 @@
def startTime = System.currentTimeMillis()
try {
// Get entity definition for field descriptions
def entityDef = ec.entity.getEntityDefinition(entityName)
// Query entity data (limited to prevent large responses)
def entityList = ec.entity.find(entityName)
.limit(100)
......@@ -543,6 +571,18 @@
def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
// Build field info for LLM
def fieldInfo = []
entityDef.allFieldInfoList.each { field ->
fieldInfo << [
name: field.name,
type: field.type,
description: field.description ?: "",
isPk: field.isPk,
required: field.notNull
]
}
// Convert to MCP resource content
def contents = [
[
......@@ -550,7 +590,10 @@
mimeType: "application/json",
text: new JsonBuilder([
entityName: entityName,
description: entityDef.description ?: "",
packageName: entityDef.packageName,
recordCount: entityList.size(),
fields: fieldInfo,
data: entityList
]).toString()
]
......
<?xml version="1.0" encoding="UTF-8"?>
<!-- This software is in the public domain under CC0 1.0 Universal plus a
Grant of Patent License.
To the extent possible under law, 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
<https://creativecommons.org/publicdomain/zero/1.0/>. -->
<resource xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/rest-api-3.xsd"
name="mcp" displayName="MCP API" version="2.0.0"
description="MCP API services for Moqui integration - NOTE: Main functionality moved to screen/webapp.xml">
<!-- NOTE: Main MCP functionality moved to screen/webapp.xml for unified JSON-RPC and SSE handling -->
<!-- Keeping only basic GET endpoints for health checks and debugging -->
<resource name="rpc">
<method type="get">
<service name="McpServices.mcp#Ping"/>
</method>
<method type="get" path="debug">
<service name="McpServices.debug#ComponentStatus"/>
</method>
</resource>
</resource>
/*
* This software is in the public domain under CC0 1.0 Universal plus a
* Grant of Patent License.
*
* To the extent possible under law, 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/>.
*/
package org.moqui.mcp
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.servlet.*
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
class McpFilter implements Filter {
protected final static Logger logger = LoggerFactory.getLogger(McpFilter.class)
private MoquiMcpServlet mcpServlet = new MoquiMcpServlet()
@Override
void init(FilterConfig filterConfig) throws ServletException {
logger.info("========== MCP FILTER INITIALIZED ==========")
// Initialize the servlet with filter config
mcpServlet.init(new ServletConfig() {
@Override
String getServletName() { return "McpFilter" }
@Override
ServletContext getServletContext() { return filterConfig.getServletContext() }
@Override
String getInitParameter(String name) { return filterConfig.getInitParameter(name) }
@Override
Enumeration<String> getInitParameterNames() { return filterConfig.getInitParameterNames() }
})
}
@Override
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
logger.info("========== MCP FILTER DOFILTER CALLED ==========")
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
HttpServletRequest httpRequest = (HttpServletRequest) request
HttpServletResponse httpResponse = (HttpServletResponse) response
// Check if this is an MCP request
String path = httpRequest.getRequestURI()
logger.info("========== MCP FILTER PATH: {} ==========", path)
if (path != null && path.contains("/mcpservlet")) {
logger.info("========== MCP FILTER HANDLING REQUEST ==========")
try {
// Handle MCP request directly, don't continue chain
mcpServlet.service(httpRequest, httpResponse)
return
} catch (Exception e) {
logger.error("Error in MCP filter", e)
// Send error response directly
httpResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
httpResponse.setContentType("application/json")
httpResponse.writer.write(groovy.json.JsonOutput.toJson([
jsonrpc: "2.0",
error: [code: -32603, message: "Internal error: " + e.message],
id: null
]))
return
}
}
}
// Not an MCP request, continue chain
chain.doFilter(request, response)
}
@Override
void destroy() {
mcpServlet.destroy()
logger.info("McpFilter destroyed")
}
}
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<!--
This software is in the public domain under CC0 1.0 Universal plus a
Grant of Patent License.
To the extent possible under law, 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/>.
-->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!-- Service-Based MCP Servlet Configuration -->
<servlet>
<servlet-name>ServiceBasedMcpServlet</servlet-name>
<servlet-class>org.moqui.mcp.ServiceBasedMcpServlet</servlet-class>
<!-- Configuration Parameters -->
<init-param>
<param-name>sseEndpoint</param-name>
<param-value>/sse</param-value>
</init-param>
<init-param>
<param-name>messageEndpoint</param-name>
<param-value>/mcp/message</param-value>
</init-param>
<init-param>
<param-name>keepAliveIntervalSeconds</param-name>
<param-value>30</param-value>
</init-param>
<init-param>
<param-name>maxConnections</param-name>
<param-value>100</param-value>
</init-param>
<!-- Enable async support for SSE -->
<async-supported>true</async-supported>
<!-- Load on startup -->
<load-on-startup>5</load-on-startup>
</servlet>
<!-- Servlet Mappings -->
<servlet-mapping>
<servlet-name>ServiceBasedMcpServlet</servlet-name>
<url-pattern>/sse/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>ServiceBasedMcpServlet</servlet-name>
<url-pattern>/mcp/message/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>ServiceBasedMcpServlet</servlet-name>
<url-pattern>/rpc/*</url-pattern>
</servlet-mapping>
<!-- Session Configuration -->
<session-config>
<session-timeout>30</session-timeout>
<cookie-config>
<http-only>true</http-only>
<secure>false</secure>
</cookie-config>
</session-config>
<!-- Security Constraints (optional - uncomment if needed) -->
<!--
<security-constraint>
<web-resource-collection>
<web-resource-name>MCP Endpoints</web-resource-name>
<url-pattern>/sse/*</url-pattern>
<url-pattern>/mcp/message/*</url-pattern>
<url-pattern>/rpc/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>Moqui MCP</realm-name>
</login-config>
-->
<!-- MIME Type Mappings -->
<mime-mapping>
<extension>json</extension>
<mime-type>application/json</mime-type>
</mime-mapping>
<!-- Default Welcome Files -->
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!-- MCP SDK Services Servlet Configuration -->
<servlet>
<servlet-name>McpSdkServicesServlet</servlet-name>
<servlet-class>org.moqui.mcp.McpSdkServicesServlet</servlet-class>
<!-- Configuration parameters -->
<init-param>
<param-name>moqui-name</param-name>
<param-value>moqui-mcp-2</param-value>
</init-param>
<!-- Enable async support (required for SSE) -->
<async-supported>true</async-supported>
<!-- Load on startup to initialize MCP server -->
<load-on-startup>1</load-on-startup>
</servlet>
<!-- Servlet mappings for MCP SDK Services endpoints -->
<!-- The MCP SDK will handle both SSE and message endpoints through this servlet -->
<servlet-mapping>
<servlet-name>McpSdkServicesServlet</servlet-name>
<url-pattern>/mcp/*</url-pattern>
</servlet-mapping>
<!-- Session configuration -->
<session-config>
<session-timeout>30</session-timeout>
<cookie-config>
<http-only>true</http-only>
<secure>false</secure>
</cookie-config>
</session-config>
</web-app>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!-- MCP SDK SSE Servlet Configuration -->
<servlet>
<servlet-name>McpSdkSseServlet</servlet-name>
<servlet-class>org.moqui.mcp.McpSdkSseServlet</servlet-class>
<!-- Configuration parameters -->
<init-param>
<param-name>moqui-name</param-name>
<param-value>moqui-mcp-2</param-value>
</init-param>
<!-- Enable async support (required for SSE) -->
<async-supported>true</async-supported>
<!-- Load on startup to initialize MCP server -->
<load-on-startup>1</load-on-startup>
</servlet>
<!-- Servlet mappings for MCP SDK SSE endpoints -->
<!-- The MCP SDK will handle both SSE and message endpoints through this servlet -->
<servlet-mapping>
<servlet-name>McpSdkSseServlet</servlet-name>
<url-pattern>/mcp/*</url-pattern>
</servlet-mapping>
<!-- Session configuration -->
<session-config>
<session-timeout>30</session-timeout>
<cookie-config>
<http-only>true</http-only>
<secure>false</secure>
</cookie-config>
</session-config>
</web-app>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!-- MCP SSE Servlet Configuration -->
<servlet>
<servlet-name>McpSseServlet</servlet-name>
<servlet-class>org.moqui.mcp.McpSseServlet</servlet-class>
<!-- Configuration parameters -->
<init-param>
<param-name>moqui-name</param-name>
<param-value>moqui-mcp-2</param-value>
</init-param>
<init-param>
<param-name>sseEndpoint</param-name>
<param-value>/sse</param-value>
</init-param>
<init-param>
<param-name>messageEndpoint</param-name>
<param-value>/mcp/message</param-value>
</init-param>
<init-param>
<param-name>keepAliveIntervalSeconds</param-name>
<param-value>30</param-value>
</init-param>
<init-param>
<param-name>maxConnections</param-name>
<param-value>100</param-value>
</init-param>
<!-- Enable async support -->
<async-supported>true</async-supported>
</servlet>
<!-- Servlet mappings for MCP SSE endpoints -->
<servlet-mapping>
<servlet-name>McpSseServlet</servlet-name>
<url-pattern>/sse/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>McpSseServlet</servlet-name>
<url-pattern>/mcp/message/*</url-pattern>
</servlet-mapping>
<!-- Session configuration -->
<session-config>
<session-timeout>30</session-timeout>
<cookie-config>
<http-only>true</http-only>
<secure>false</secure>
</cookie-config>
</session-config>
</web-app>
\ No newline at end of file