Consolidate MCP implementation with shared JSON-RPC classes
- Extract JsonRpcMessage classes to separate file for better code organization - Remove deprecated McpSessionManager (unused, replaced by Visit-based sessions) - Remove problematic ServiceBasedMcpServlet (async limitations, service invocation bugs) - Enhance EnhancedMcpServlet with configuration parameters and improved monitoring - Add broadcast success/failure counting and helper methods - Fix variable scope issue with requestBody in JSON-RPC handler - Consolidate to single, working MCP servlet implementation Working features: - Authentication with Basic auth - SSE connections with proper session management - JSON-RPC protocol (ping, initialize, tools/list) - Visit-based session persistence - Service delegation to McpServices.xml
Showing
4 changed files
with
116 additions
and
326 deletions
| ... | @@ -32,46 +32,7 @@ import java.util.concurrent.ConcurrentHashMap | ... | @@ -32,46 +32,7 @@ import java.util.concurrent.ConcurrentHashMap |
| 32 | import java.util.concurrent.atomic.AtomicBoolean | 32 | import java.util.concurrent.atomic.AtomicBoolean |
| 33 | import java.util.UUID | 33 | import java.util.UUID |
| 34 | 34 | ||
| 35 | /** | ||
| 36 | * Simple JSON-RPC Message classes for MCP compatibility | ||
| 37 | */ | ||
| 38 | class JsonRpcMessage { | ||
| 39 | String jsonrpc = "2.0" | ||
| 40 | } | ||
| 41 | |||
| 42 | class JsonRpcResponse extends JsonRpcMessage { | ||
| 43 | Object id | ||
| 44 | Object result | ||
| 45 | Map error | ||
| 46 | |||
| 47 | JsonRpcResponse(Object result, Object id) { | ||
| 48 | this.result = result | ||
| 49 | this.id = id | ||
| 50 | } | ||
| 51 | 35 | ||
| 52 | JsonRpcResponse(Map error, Object id) { | ||
| 53 | this.error = error | ||
| 54 | this.id = id | ||
| 55 | } | ||
| 56 | |||
| 57 | String toJson() { | ||
| 58 | return JsonOutput.toJson(this) | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | class JsonRpcNotification extends JsonRpcMessage { | ||
| 63 | String method | ||
| 64 | Object params | ||
| 65 | |||
| 66 | JsonRpcNotification(String method, Object params = null) { | ||
| 67 | this.method = method | ||
| 68 | this.params = params | ||
| 69 | } | ||
| 70 | |||
| 71 | String toJson() { | ||
| 72 | return JsonOutput.toJson(this) | ||
| 73 | } | ||
| 74 | } | ||
| 75 | 36 | ||
| 76 | /** | 37 | /** |
| 77 | * Enhanced MCP Servlet with proper SSE handling inspired by HttpServletSseServerTransportProvider | 38 | * Enhanced MCP Servlet with proper SSE handling inspired by HttpServletSseServerTransportProvider |
| ... | @@ -88,12 +49,28 @@ class EnhancedMcpServlet extends HttpServlet { | ... | @@ -88,12 +49,28 @@ class EnhancedMcpServlet extends HttpServlet { |
| 88 | // Session management using Moqui's Visit system directly | 49 | // Session management using Moqui's Visit system directly |
| 89 | // No need for separate session manager - Visit entity handles persistence | 50 | // No need for separate session manager - Visit entity handles persistence |
| 90 | 51 | ||
| 52 | // Configuration parameters | ||
| 53 | private String sseEndpoint = "/sse" | ||
| 54 | private String messageEndpoint = "/message" | ||
| 55 | private int keepAliveIntervalSeconds = 30 | ||
| 56 | private int maxConnections = 100 | ||
| 57 | |||
| 91 | @Override | 58 | @Override |
| 92 | void init(ServletConfig config) throws ServletException { | 59 | void init(ServletConfig config) throws ServletException { |
| 93 | super.init(config) | 60 | super.init(config) |
| 61 | |||
| 62 | // Read configuration from servlet init parameters | ||
| 63 | sseEndpoint = config.getInitParameter("sseEndpoint") ?: sseEndpoint | ||
| 64 | messageEndpoint = config.getInitParameter("messageEndpoint") ?: messageEndpoint | ||
| 65 | keepAliveIntervalSeconds = config.getInitParameter("keepAliveIntervalSeconds")?.toInteger() ?: keepAliveIntervalSeconds | ||
| 66 | maxConnections = config.getInitParameter("maxConnections")?.toInteger() ?: maxConnections | ||
| 67 | |||
| 94 | String webappName = config.getInitParameter("moqui-name") ?: | 68 | String webappName = config.getInitParameter("moqui-name") ?: |
| 95 | config.getServletContext().getInitParameter("moqui-name") | 69 | config.getServletContext().getInitParameter("moqui-name") |
| 70 | |||
| 96 | logger.info("EnhancedMcpServlet initialized for webapp ${webappName}") | 71 | logger.info("EnhancedMcpServlet initialized for webapp ${webappName}") |
| 72 | logger.info("SSE endpoint: ${sseEndpoint}, Message endpoint: ${messageEndpoint}") | ||
| 73 | logger.info("Keep-alive interval: ${keepAliveIntervalSeconds}s, Max connections: ${maxConnections}") | ||
| 97 | } | 74 | } |
| 98 | 75 | ||
| 99 | @Override | 76 | @Override |
| ... | @@ -542,11 +519,6 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -542,11 +519,6 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 542 | 519 | ||
| 543 | logger.info("Enhanced MCP JSON-RPC Request: ${method} ${request.requestURI} - Accept: ${acceptHeader}, Content-Type: ${contentType}") | 520 | logger.info("Enhanced MCP JSON-RPC Request: ${method} ${request.requestURI} - Accept: ${acceptHeader}, Content-Type: ${contentType}") |
| 544 | 521 | ||
| 545 | // Log request body for debugging (be careful with this in production) | ||
| 546 | if (requestBody?.length() > 0) { | ||
| 547 | logger.info("MCP JSON-RPC request body: ${requestBody}") | ||
| 548 | } | ||
| 549 | |||
| 550 | // Handle POST requests for JSON-RPC | 522 | // Handle POST requests for JSON-RPC |
| 551 | if (!"POST".equals(method)) { | 523 | if (!"POST".equals(method)) { |
| 552 | response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED) | 524 | response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED) |
| ... | @@ -593,6 +565,11 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -593,6 +565,11 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 593 | return | 565 | return |
| 594 | } | 566 | } |
| 595 | 567 | ||
| 568 | // Log request body for debugging (be careful with this in production) | ||
| 569 | if (requestBody.length() > 0) { | ||
| 570 | logger.info("MCP JSON-RPC request body: ${requestBody}") | ||
| 571 | } | ||
| 572 | |||
| 596 | def rpcRequest | 573 | def rpcRequest |
| 597 | try { | 574 | try { |
| 598 | rpcRequest = jsonSlurper.parseText(requestBody) | 575 | rpcRequest = jsonSlurper.parseText(requestBody) |
| ... | @@ -777,25 +754,55 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -777,25 +754,55 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 777 | 754 | ||
| 778 | logger.info("Broadcasting to ${mcpVisits.size()} MCP visits, ${activeConnections.size()} active connections") | 755 | logger.info("Broadcasting to ${mcpVisits.size()} MCP visits, ${activeConnections.size()} active connections") |
| 779 | 756 | ||
| 757 | int successCount = 0 | ||
| 758 | int failureCount = 0 | ||
| 759 | |||
| 780 | // Send to active connections (transient) | 760 | // Send to active connections (transient) |
| 781 | mcpVisits.each { visit -> | 761 | mcpVisits.each { visit -> |
| 782 | PrintWriter writer = activeConnections.get(visit.visitId) | 762 | PrintWriter writer = activeConnections.get(visit.visitId) |
| 783 | if (writer && !writer.checkError()) { | 763 | if (writer && !writer.checkError()) { |
| 784 | try { | 764 | try { |
| 785 | sendSseEvent(writer, "broadcast", message.toJson()) | 765 | sendSseEvent(writer, "broadcast", message.toJson()) |
| 766 | successCount++ | ||
| 786 | } catch (Exception e) { | 767 | } catch (Exception e) { |
| 787 | logger.warn("Failed to send broadcast to ${visit.visitId}: ${e.message}") | 768 | logger.warn("Failed to send broadcast to ${visit.visitId}: ${e.message}") |
| 788 | // Remove broken connection | 769 | // Remove broken connection |
| 789 | activeConnections.remove(visit.visitId) | 770 | activeConnections.remove(visit.visitId) |
| 771 | failureCount++ | ||
| 790 | } | 772 | } |
| 773 | } else { | ||
| 774 | // No active connection for this visit | ||
| 775 | failureCount++ | ||
| 791 | } | 776 | } |
| 792 | } | 777 | } |
| 778 | |||
| 779 | logger.info("Broadcast completed: ${successCount} successful, ${failureCount} failed") | ||
| 780 | |||
| 793 | } catch (Exception e) { | 781 | } catch (Exception e) { |
| 794 | logger.error("Error broadcasting to all sessions: ${e.message}", e) | 782 | logger.error("Error broadcasting to all sessions: ${e.message}", e) |
| 795 | } | 783 | } |
| 796 | } | 784 | } |
| 797 | 785 | ||
| 798 | /** | 786 | /** |
| 787 | * Send SSE event to specific session (helper method) | ||
| 788 | */ | ||
| 789 | void sendToSession(String sessionId, JsonRpcMessage message) { | ||
| 790 | try { | ||
| 791 | PrintWriter writer = activeConnections.get(sessionId) | ||
| 792 | if (writer && !writer.checkError()) { | ||
| 793 | sendSseEvent(writer, "message", message.toJson()) | ||
| 794 | logger.debug("Sent message to session ${sessionId}") | ||
| 795 | } else { | ||
| 796 | logger.warn("No active connection for session ${sessionId}") | ||
| 797 | } | ||
| 798 | } catch (Exception e) { | ||
| 799 | logger.error("Error sending message to session ${sessionId}: ${e.message}", e) | ||
| 800 | // Remove broken connection | ||
| 801 | activeConnections.remove(sessionId) | ||
| 802 | } | ||
| 803 | } | ||
| 804 | |||
| 805 | /** | ||
| 799 | * Get session statistics for monitoring | 806 | * Get session statistics for monitoring |
| 800 | */ | 807 | */ |
| 801 | Map getSessionStatistics() { | 808 | Map getSessionStatistics() { |
| ... | @@ -808,12 +815,22 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -808,12 +815,22 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 808 | return [ | 815 | return [ |
| 809 | totalMcpVisits: mcpVisits.size(), | 816 | totalMcpVisits: mcpVisits.size(), |
| 810 | activeConnections: activeConnections.size(), | 817 | activeConnections: activeConnections.size(), |
| 818 | maxConnections: maxConnections, | ||
| 811 | architecture: "Visit-based sessions with connection registry", | 819 | architecture: "Visit-based sessions with connection registry", |
| 812 | message: "Enhanced MCP with session tracking" | 820 | message: "Enhanced MCP with session tracking", |
| 821 | endpoints: [ | ||
| 822 | sse: sseEndpoint, | ||
| 823 | message: messageEndpoint | ||
| 824 | ], | ||
| 825 | keepAliveInterval: keepAliveIntervalSeconds | ||
| 813 | ] | 826 | ] |
| 814 | } catch (Exception e) { | 827 | } catch (Exception e) { |
| 815 | logger.error("Error getting session statistics: ${e.message}", e) | 828 | logger.error("Error getting session statistics: ${e.message}", e) |
| 816 | return [activeSessions: activeConnections.size(), error: e.message] | 829 | return [ |
| 830 | activeConnections: activeConnections.size(), | ||
| 831 | maxConnections: maxConnections, | ||
| 832 | error: e.message | ||
| 833 | ] | ||
| 817 | } | 834 | } |
| 818 | } | 835 | } |
| 819 | } | 836 | } |
| ... | \ 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 | ||
| 3 | * Grant of Patent License. | ||
| 4 | * | ||
| 5 | * To the extent possible under law, author(s) have dedicated all | ||
| 6 | * copyright and related and neighboring rights to this software to the | ||
| 7 | * public domain worldwide. This software is distributed without any | ||
| 8 | * warranty. | ||
| 9 | * | ||
| 10 | * You should have received a copy of the CC0 Public Domain Dedication | ||
| 11 | * along with this software (see the LICENSE.md file). If not, see | ||
| 12 | * <http://creativecommons.org/publicdomain/zero/1.0/>. | ||
| 13 | */ | ||
| 14 | package org.moqui.mcp | ||
| 15 | |||
| 16 | import groovy.json.JsonOutput | ||
| 17 | |||
| 18 | /** | ||
| 19 | * Simple JSON-RPC Message classes for MCP compatibility | ||
| 20 | */ | ||
| 21 | class JsonRpcMessage { | ||
| 22 | String jsonrpc = "2.0" | ||
| 23 | |||
| 24 | String toJson() { | ||
| 25 | return JsonOutput.toJson(this) | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | class JsonRpcResponse extends JsonRpcMessage { | ||
| 30 | Object id | ||
| 31 | Object result | ||
| 32 | Map error | ||
| 33 | |||
| 34 | JsonRpcResponse(Object result, Object id) { | ||
| 35 | this.result = result | ||
| 36 | this.id = id | ||
| 37 | } | ||
| 38 | |||
| 39 | JsonRpcResponse(Map error, Object id) { | ||
| 40 | this.error = error | ||
| 41 | this.id = id | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | class JsonRpcNotification extends JsonRpcMessage { | ||
| 46 | String method | ||
| 47 | Object params | ||
| 48 | |||
| 49 | JsonRpcNotification(String method, Object params = null) { | ||
| 50 | this.method = method | ||
| 51 | this.params = params | ||
| 52 | } | ||
| 53 | } | ||
| ... | \ 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 | ||
| 3 | * Grant of Patent License. | ||
| 4 | * | ||
| 5 | * To the extent possible under law, author(s) have dedicated all | ||
| 6 | * copyright and related and neighboring rights to this software to the | ||
| 7 | * public domain worldwide. This software is distributed without any | ||
| 8 | * warranty. | ||
| 9 | * | ||
| 10 | * You should have received a copy of the CC0 Public Domain Dedication | ||
| 11 | * along with this software (see the LICENSE.md file). If not, see | ||
| 12 | * <http://creativecommons.org/publicdomain/zero/1.0/>. | ||
| 13 | */ | ||
| 14 | package org.moqui.mcp | ||
| 15 | |||
| 16 | import org.slf4j.Logger | ||
| 17 | import org.slf4j.LoggerFactory | ||
| 18 | |||
| 19 | import java.util.concurrent.ConcurrentHashMap | ||
| 20 | import java.util.concurrent.Executors | ||
| 21 | import java.util.concurrent.ScheduledExecutorService | ||
| 22 | import java.util.concurrent.TimeUnit | ||
| 23 | import java.util.concurrent.atomic.AtomicBoolean | ||
| 24 | |||
| 25 | /** | ||
| 26 | * MCP Session Manager with SDK-style capabilities | ||
| 27 | * | ||
| 28 | * @deprecated This class is deprecated. Use Moqui's Visit entity directly for session management. | ||
| 29 | * See VisitBasedMcpSession for the new Visit-based approach. | ||
| 30 | * | ||
| 31 | * Provides centralized session management, broadcasting, and graceful shutdown | ||
| 32 | */ | ||
| 33 | @Deprecated | ||
| 34 | class McpSessionManager { | ||
| 35 | protected final static Logger logger = LoggerFactory.getLogger(McpSessionManager.class) | ||
| 36 | |||
| 37 | private final Map<String, VisitBasedMcpSession> sessions = new ConcurrentHashMap<>() | ||
| 38 | private final AtomicBoolean isShuttingDown = new AtomicBoolean(false) | ||
| 39 | private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2) | ||
| 40 | |||
| 41 | // Session cleanup and monitoring | ||
| 42 | private final long sessionTimeoutMs = 30 * 60 * 1000 // 30 minutes | ||
| 43 | private final long cleanupIntervalMs = 5 * 60 * 1000 // 5 minutes | ||
| 44 | |||
| 45 | McpSessionManager() { | ||
| 46 | // Start periodic cleanup task | ||
| 47 | scheduler.scheduleAtFixedRate(this::cleanupInactiveSessions, | ||
| 48 | cleanupIntervalMs, cleanupIntervalMs, TimeUnit.MILLISECONDS) | ||
| 49 | |||
| 50 | logger.info("MCP Session Manager initialized") | ||
| 51 | } | ||
| 52 | |||
| 53 | /** | ||
| 54 | * Register a new session | ||
| 55 | */ | ||
| 56 | void registerSession(VisitBasedMcpSession session) { | ||
| 57 | if (isShuttingDown.get()) { | ||
| 58 | logger.warn("Rejecting session registration during shutdown: ${session.sessionId}") | ||
| 59 | return | ||
| 60 | } | ||
| 61 | |||
| 62 | sessions.put(session.sessionId, session) | ||
| 63 | logger.info("Registered MCP session ${session.sessionId} (total: ${sessions.size()})") | ||
| 64 | |||
| 65 | // Send welcome message to new session | ||
| 66 | def welcomeMessage = new JsonRpcNotification("welcome", [ | ||
| 67 | sessionId: session.sessionId, | ||
| 68 | totalSessions: sessions.size(), | ||
| 69 | timestamp: System.currentTimeMillis() | ||
| 70 | ]) | ||
| 71 | session.sendMessage(welcomeMessage) | ||
| 72 | } | ||
| 73 | |||
| 74 | /** | ||
| 75 | * Unregister a session | ||
| 76 | */ | ||
| 77 | void unregisterSession(String sessionId) { | ||
| 78 | def session = sessions.remove(sessionId) | ||
| 79 | if (session) { | ||
| 80 | logger.info("Unregistered MCP session ${sessionId} (remaining: ${sessions.size()})") | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | /** | ||
| 85 | * Get session by ID | ||
| 86 | */ | ||
| 87 | VisitBasedMcpSession getSession(String sessionId) { | ||
| 88 | return sessions.get(sessionId) | ||
| 89 | } | ||
| 90 | |||
| 91 | /** | ||
| 92 | * Broadcast message to all active sessions | ||
| 93 | */ | ||
| 94 | void broadcast(JsonRpcMessage message) { | ||
| 95 | if (isShuttingDown.get()) { | ||
| 96 | logger.warn("Rejecting broadcast during shutdown") | ||
| 97 | return | ||
| 98 | } | ||
| 99 | |||
| 100 | def inactiveSessions = [] | ||
| 101 | def activeCount = 0 | ||
| 102 | |||
| 103 | sessions.values().each { session -> | ||
| 104 | try { | ||
| 105 | if (session.isActive()) { | ||
| 106 | session.sendMessage(message) | ||
| 107 | activeCount++ | ||
| 108 | } else { | ||
| 109 | inactiveSessions << session.sessionId | ||
| 110 | } | ||
| 111 | } catch (Exception e) { | ||
| 112 | logger.warn("Error broadcasting to session ${session.sessionId}: ${e.message}") | ||
| 113 | inactiveSessions << session.sessionId | ||
| 114 | } | ||
| 115 | } | ||
| 116 | |||
| 117 | // Clean up inactive sessions | ||
| 118 | inactiveSessions.each { sessionId -> | ||
| 119 | unregisterSession(sessionId) | ||
| 120 | } | ||
| 121 | |||
| 122 | logger.info("Broadcast message to ${activeCount} active sessions (removed ${inactiveSessions.size()} inactive)") | ||
| 123 | } | ||
| 124 | |||
| 125 | /** | ||
| 126 | * Send message to specific session | ||
| 127 | */ | ||
| 128 | boolean sendToSession(String sessionId, JsonRpcMessage message) { | ||
| 129 | def session = sessions.get(sessionId) | ||
| 130 | if (!session) { | ||
| 131 | return false | ||
| 132 | } | ||
| 133 | |||
| 134 | try { | ||
| 135 | if (session.isActive()) { | ||
| 136 | session.sendMessage(message) | ||
| 137 | return true | ||
| 138 | } else { | ||
| 139 | unregisterSession(sessionId) | ||
| 140 | return false | ||
| 141 | } | ||
| 142 | } catch (Exception e) { | ||
| 143 | logger.warn("Error sending to session ${sessionId}: ${e.message}") | ||
| 144 | unregisterSession(sessionId) | ||
| 145 | return false | ||
| 146 | } | ||
| 147 | } | ||
| 148 | |||
| 149 | /** | ||
| 150 | * Get session statistics | ||
| 151 | */ | ||
| 152 | Map getSessionStatistics() { | ||
| 153 | def stats = [ | ||
| 154 | totalSessions: sessions.size(), | ||
| 155 | activeSessions: 0, | ||
| 156 | closingSessions: 0, | ||
| 157 | isShuttingDown: isShuttingDown.get(), | ||
| 158 | uptime: System.currentTimeMillis() - (this.@startTime ?: System.currentTimeMillis()), | ||
| 159 | sessions: [] | ||
| 160 | ] | ||
| 161 | |||
| 162 | sessions.values().each { session -> | ||
| 163 | def sessionStats = session.getSessionStats() | ||
| 164 | stats.sessions << sessionStats | ||
| 165 | |||
| 166 | if (sessionStats.active) { | ||
| 167 | stats.activeSessions++ | ||
| 168 | } | ||
| 169 | if (sessionStats.closing) { | ||
| 170 | stats.closingSessions++ | ||
| 171 | } | ||
| 172 | } | ||
| 173 | |||
| 174 | return stats | ||
| 175 | } | ||
| 176 | |||
| 177 | /** | ||
| 178 | * Initiate graceful shutdown | ||
| 179 | */ | ||
| 180 | void shutdownGracefully() { | ||
| 181 | if (!isShuttingDown.compareAndSet(false, true)) { | ||
| 182 | return // Already shutting down | ||
| 183 | } | ||
| 184 | |||
| 185 | logger.info("Initiating graceful MCP session manager shutdown") | ||
| 186 | |||
| 187 | // Send shutdown notification to all sessions | ||
| 188 | def shutdownMessage = new JsonRpcNotification("server_shutdown", [ | ||
| 189 | message: "Server is shutting down gracefully", | ||
| 190 | timestamp: System.currentTimeMillis() | ||
| 191 | ]) | ||
| 192 | broadcast(shutdownMessage) | ||
| 193 | |||
| 194 | // Give sessions time to receive shutdown message | ||
| 195 | scheduler.schedule({ | ||
| 196 | forceShutdown() | ||
| 197 | }, 5, TimeUnit.SECONDS) | ||
| 198 | } | ||
| 199 | |||
| 200 | /** | ||
| 201 | * Force immediate shutdown | ||
| 202 | */ | ||
| 203 | void forceShutdown() { | ||
| 204 | logger.info("Force shutting down MCP session manager") | ||
| 205 | |||
| 206 | // Close all sessions | ||
| 207 | sessions.values().each { session -> | ||
| 208 | try { | ||
| 209 | session.close() | ||
| 210 | } catch (Exception e) { | ||
| 211 | logger.warn("Error closing session ${session.sessionId}: ${e.message}") | ||
| 212 | } | ||
| 213 | } | ||
| 214 | sessions.clear() | ||
| 215 | |||
| 216 | // Shutdown scheduler | ||
| 217 | scheduler.shutdown() | ||
| 218 | try { | ||
| 219 | if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) { | ||
| 220 | scheduler.shutdownNow() | ||
| 221 | } | ||
| 222 | } catch (InterruptedException e) { | ||
| 223 | scheduler.shutdownNow() | ||
| 224 | Thread.currentThread().interrupt() | ||
| 225 | } | ||
| 226 | |||
| 227 | logger.info("MCP session manager shutdown complete") | ||
| 228 | } | ||
| 229 | |||
| 230 | /** | ||
| 231 | * Clean up inactive sessions | ||
| 232 | */ | ||
| 233 | private void cleanupInactiveSessions() { | ||
| 234 | if (isShuttingDown.get()) { | ||
| 235 | return | ||
| 236 | } | ||
| 237 | |||
| 238 | def now = System.currentTimeMillis() | ||
| 239 | def inactiveSessions = [] | ||
| 240 | |||
| 241 | sessions.values().each { session -> | ||
| 242 | def sessionStats = session.getSessionStats() | ||
| 243 | def inactiveTime = now - (sessionStats.lastActivity ?: sessionStats.createdAt.time) | ||
| 244 | |||
| 245 | if (!session.isActive() || inactiveTime > sessionTimeoutMs) { | ||
| 246 | inactiveSessions << session.sessionId | ||
| 247 | } | ||
| 248 | } | ||
| 249 | |||
| 250 | inactiveSessions.each { sessionId -> | ||
| 251 | def session = sessions.get(sessionId) | ||
| 252 | if (session) { | ||
| 253 | try { | ||
| 254 | session.closeGracefully() | ||
| 255 | } catch (Exception e) { | ||
| 256 | logger.warn("Error during cleanup of session ${sessionId}: ${e.message}") | ||
| 257 | } | ||
| 258 | unregisterSession(sessionId) | ||
| 259 | } | ||
| 260 | } | ||
| 261 | |||
| 262 | if (inactiveSessions.size() > 0) { | ||
| 263 | logger.info("Cleaned up ${inactiveSessions.size()} inactive MCP sessions") | ||
| 264 | } | ||
| 265 | } | ||
| 266 | |||
| 267 | /** | ||
| 268 | * Get active session count | ||
| 269 | */ | ||
| 270 | int getActiveSessionCount() { | ||
| 271 | return (int) sessions.values().count { it.isActive() } | ||
| 272 | } | ||
| 273 | |||
| 274 | /** | ||
| 275 | * Check if manager is shutting down | ||
| 276 | */ | ||
| 277 | boolean isShuttingDown() { | ||
| 278 | return isShuttingDown.get() | ||
| 279 | } | ||
| 280 | } | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
This diff is collapsed.
Click to expand it.
-
Please register or sign in to post a comment