MoquiNotificationMcpBridge.groovy
9.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
/*
* 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.adapter
import org.moqui.context.ExecutionContextFactory
import org.moqui.context.NotificationMessage
import org.moqui.context.NotificationMessageListener
import org.moqui.mcp.transport.MoquiMcpTransport
import org.slf4j.Logger
import org.slf4j.LoggerFactory
/**
* Bridge that connects Moqui's NotificationMessage system to MCP notifications.
* Implements NotificationMessageListener to receive all Moqui notifications
* and forwards them to MCP clients via the transport layer.
*/
class MoquiNotificationMcpBridge implements NotificationMessageListener {
protected final static Logger logger = LoggerFactory.getLogger(MoquiNotificationMcpBridge.class)
private ExecutionContextFactory ecf
private MoquiMcpTransport transport
// Topic prefix for MCP-specific notifications (optional filtering)
private static final String MCP_TOPIC_PREFIX = "mcp."
// Whether to forward all notifications or only MCP-prefixed ones
private boolean forwardAllNotifications = true
/**
* Initialize the bridge with the ECF and transport
* Note: This method signature matches what the ECF registration expects
*/
@Override
void init(ExecutionContextFactory ecf) {
this.ecf = ecf
logger.info("MoquiNotificationMcpBridge initialized (transport not yet set)")
}
/**
* Set the transport after initialization
* @param transport The MCP transport to use for sending notifications
*/
void setTransport(MoquiMcpTransport transport) {
this.transport = transport
logger.info("MoquiNotificationMcpBridge transport configured: ${transport?.class?.simpleName}")
}
/**
* Configure whether to forward all notifications or only MCP-prefixed ones
* @param forwardAll If true, forward all notifications; if false, only forward those with topic starting with 'mcp.'
*/
void setForwardAllNotifications(boolean forwardAll) {
this.forwardAllNotifications = forwardAll
logger.info("MoquiNotificationMcpBridge forwardAllNotifications set to: ${forwardAll}")
}
@Override
void onMessage(NotificationMessage nm) {
if (transport == null) {
logger.trace("Transport not configured, skipping notification: ${nm.topic}")
return
}
// Optionally filter by topic prefix
if (!forwardAllNotifications && !nm.topic?.startsWith(MCP_TOPIC_PREFIX)) {
logger.trace("Skipping non-MCP notification: ${nm.topic}")
return
}
try {
// Convert Moqui notification → MCP notification format
Map mcpNotification = convertToMcpNotification(nm)
// Get target users
Set<String> notifyUserIds = nm.getNotifyUserIds()
if (notifyUserIds && !notifyUserIds.isEmpty()) {
// Send to each target user's active MCP sessions
int sentCount = 0
for (String userId in notifyUserIds) {
try {
transport.sendNotificationToUser(userId, mcpNotification)
sentCount++
logger.debug("Sent MCP notification to user ${userId}: ${nm.topic}")
} catch (Exception e) {
logger.warn("Failed to send MCP notification to user ${userId}: ${e.message}")
}
}
logger.info("Forwarded Moqui notification '${nm.topic}' to ${sentCount} users via MCP")
} else {
// No specific users, could broadcast or log
logger.debug("Notification '${nm.topic}' has no target users, skipping MCP forward")
}
} catch (Exception e) {
logger.error("Error converting/sending Moqui notification to MCP: ${e.message}", e)
}
}
/**
* Convert a Moqui NotificationMessage to MCP notification format
* @param nm The Moqui notification
* @return The MCP notification map
*/
private Map convertToMcpNotification(NotificationMessage nm) {
return [
jsonrpc: "2.0",
method: "notifications/message",
params: [
topic: nm.topic,
subTopic: nm.subTopic,
title: nm.title,
type: nm.type,
message: nm.getMessageMap() ?: [:],
link: nm.link,
showAlert: nm.isShowAlert(),
notificationMessageId: nm.notificationMessageId,
timestamp: System.currentTimeMillis()
]
]
}
/**
* Create a custom MCP notification and send to specific users
* @param topic The notification topic
* @param title The notification title
* @param message The message content
* @param userIds The target user IDs
*/
void sendMcpNotification(String topic, String title, Map message, Set<String> userIds) {
if (transport == null) {
logger.warn("Cannot send MCP notification: transport not configured")
return
}
Map mcpNotification = [
jsonrpc: "2.0",
method: "notifications/message",
params: [
topic: topic,
title: title,
message: message,
timestamp: System.currentTimeMillis()
]
]
for (String userId in userIds) {
try {
transport.sendNotificationToUser(userId, mcpNotification)
logger.debug("Sent custom MCP notification to user ${userId}: ${topic}")
} catch (Exception e) {
logger.warn("Failed to send custom MCP notification to user ${userId}: ${e.message}")
}
}
}
/**
* Broadcast an MCP notification to all active sessions
* @param topic The notification topic
* @param title The notification title
* @param message The message content
*/
void broadcastMcpNotification(String topic, String title, Map message) {
if (transport == null) {
logger.warn("Cannot broadcast MCP notification: transport not configured")
return
}
Map mcpNotification = [
jsonrpc: "2.0",
method: "notifications/message",
params: [
topic: topic,
title: title,
message: message,
timestamp: System.currentTimeMillis()
]
]
try {
transport.broadcastNotification(mcpNotification)
logger.info("Broadcast MCP notification: ${topic}")
} catch (Exception e) {
logger.error("Failed to broadcast MCP notification: ${e.message}", e)
}
}
/**
* Send a tools/list_changed notification to inform clients that available tools have changed
*/
void notifyToolsChanged() {
if (transport == null) {
logger.warn("Cannot send tools changed notification: transport not configured")
return
}
Map notification = [
jsonrpc: "2.0",
method: "notifications/tools/list_changed",
params: [:]
]
try {
transport.broadcastNotification(notification)
logger.info("Broadcast tools/list_changed notification")
} catch (Exception e) {
logger.error("Failed to broadcast tools changed notification: ${e.message}", e)
}
}
/**
* Send a resources/list_changed notification
*/
void notifyResourcesChanged() {
if (transport == null) return
Map notification = [
jsonrpc: "2.0",
method: "notifications/resources/list_changed",
params: [:]
]
try {
transport.broadcastNotification(notification)
logger.info("Broadcast resources/list_changed notification")
} catch (Exception e) {
logger.error("Failed to broadcast resources changed notification: ${e.message}", e)
}
}
/**
* Send a progress notification for a long-running operation
* @param sessionId The target session
* @param progressToken The progress token
* @param progress Current progress value
* @param total Total progress value (optional)
*/
void sendProgressNotification(String sessionId, String progressToken, Number progress, Number total = null) {
if (transport == null) return
Map notification = [
jsonrpc: "2.0",
method: "notifications/progress",
params: [
progressToken: progressToken,
progress: progress,
total: total
]
]
try {
transport.sendNotification(sessionId, notification)
logger.debug("Sent progress notification to session ${sessionId}: ${progress}/${total ?: '?'}")
} catch (Exception e) {
logger.warn("Failed to send progress notification: ${e.message}")
}
}
@Override
void destroy() {
logger.info("MoquiNotificationMcpBridge destroyed")
this.ecf = null
this.transport = null
}
}