6bbfd372 by Ean Schuessler

Fix MCP two-step handshake with proper 202 Accepted response

- Fixed notifications/initialized to return 202 Accepted instead of 204 No Content
- Added comprehensive MCP method implementation (prompts, roots, sampling, etc.)
- Enhanced notification handling with proper session state transitions
- Updated protocol version support to include 2025-11-25 with backward compatibility
- Improved error handling and logging for debugging MCP connections
- Added subscription tracking and message storage for advanced features
- Fixed Accept header validation per MCP 2025-11-25 specification

Resolves the critical two-step handshake issue where MCP Inspector
was not receiving the correct response for notifications/initialized.
1 parent fdb76042
...@@ -79,8 +79,8 @@ ...@@ -79,8 +79,8 @@
79 } 79 }
80 } 80 }
81 81
82 // Validate protocol version - support common MCP versions 82 // Validate protocol version - support common MCP versions with version negotiation
83 def supportedVersions = ["2025-06-18", "2024-11-05", "2024-10-07", "2023-06-05"] 83 def supportedVersions = ["2025-11-25", "2025-06-18", "2024-11-05", "2024-10-07", "2023-06-05"]
84 if (!supportedVersions.contains(protocolVersion)) { 84 if (!supportedVersions.contains(protocolVersion)) {
85 throw new Exception("Unsupported protocol version: ${protocolVersion}. Supported versions: ${supportedVersions.join(', ')}") 85 throw new Exception("Unsupported protocol version: ${protocolVersion}. Supported versions: ${supportedVersions.join(', ')}")
86 } 86 }
...@@ -1882,6 +1882,190 @@ def startTime = System.currentTimeMillis() ...@@ -1882,6 +1882,190 @@ def startTime = System.currentTimeMillis()
1882 </actions> 1882 </actions>
1883 </service> 1883 </service>
1884 1884
1885 <service verb="mcp" noun="ResourcesTemplatesList" authenticate="false" allow-remote="true" transaction-timeout="30">
1886 <description>Handle MCP resources/templates/list request</description>
1887 <in-parameters>
1888 <parameter name="sessionId"/>
1889 </in-parameters>
1890 <out-parameters>
1891 <parameter name="result" type="Map"/>
1892 </out-parameters>
1893 <actions>
1894 <script><![CDATA[
1895 import org.moqui.context.ExecutionContext
1896
1897 ExecutionContext ec = context.ec
1898
1899 // For now, return empty templates list - can be extended later
1900 def templates = []
1901
1902 result = [resourceTemplates: templates]
1903 ]]></script>
1904 </actions>
1905 </service>
1906
1907 <service verb="mcp" noun="ResourcesSubscribe" authenticate="false" allow-remote="true" transaction-timeout="30">
1908 <description>Handle MCP resources/subscribe request</description>
1909 <in-parameters>
1910 <parameter name="sessionId"/>
1911 <parameter name="uri" required="true"><description>Resource URI to subscribe to</description></parameter>
1912 </in-parameters>
1913 <out-parameters>
1914 <parameter name="result" type="Map"/>
1915 </out-parameters>
1916 <actions>
1917 <script><![CDATA[
1918 import org.moqui.context.ExecutionContext
1919
1920 ExecutionContext ec = context.ec
1921
1922 ec.logger.info("Resource subscription requested for URI: ${uri}, sessionId: ${sessionId}")
1923
1924 // For now, just return success - actual subscription tracking could be added
1925 result = [subscribed: true, uri: uri]
1926 ]]></script>
1927 </actions>
1928 </service>
1929
1930 <service verb="mcp" noun="ResourcesUnsubscribe" authenticate="false" allow-remote="true" transaction-timeout="30">
1931 <description>Handle MCP resources/unsubscribe request</description>
1932 <in-parameters>
1933 <parameter name="sessionId"/>
1934 <parameter name="uri" required="true"><description>Resource URI to unsubscribe from</description></parameter>
1935 </in-parameters>
1936 <out-parameters>
1937 <parameter name="result" type="Map"/>
1938 </out-parameters>
1939 <actions>
1940 <script><![CDATA[
1941 import org.moqui.context.ExecutionContext
1942
1943 ExecutionContext ec = context.ec
1944
1945 ec.logger.info("Resource unsubscription requested for URI: ${uri}, sessionId: ${sessionId}")
1946
1947 // For now, just return success - actual subscription tracking could be added
1948 result = [unsubscribed: true, uri: uri]
1949 ]]></script>
1950 </actions>
1951 </service>
1952
1953 <service verb="mcp" noun="PromptsList" authenticate="false" allow-remote="true" transaction-timeout="30">
1954 <description>Handle MCP prompts/list request</description>
1955 <in-parameters>
1956 <parameter name="sessionId"/>
1957 </in-parameters>
1958 <out-parameters>
1959 <parameter name="result" type="Map"/>
1960 </out-parameters>
1961 <actions>
1962 <script><![CDATA[
1963 import org.moqui.context.ExecutionContext
1964
1965 ExecutionContext ec = context.ec
1966
1967 // For now, return empty prompts list - can be extended later
1968 def prompts = []
1969
1970 result = [prompts: prompts]
1971 ]]></script>
1972 </actions>
1973 </service>
1974
1975 <service verb="mcp" noun="PromptsGet" authenticate="false" allow-remote="true" transaction-timeout="30">
1976 <description>Handle MCP prompts/get request</description>
1977 <in-parameters>
1978 <parameter name="sessionId"/>
1979 <parameter name="name" required="true"><description>Prompt name to retrieve</description></parameter>
1980 </in-parameters>
1981 <out-parameters>
1982 <parameter name="result" type="Map"/>
1983 </out-parameters>
1984 <actions>
1985 <script><![CDATA[
1986 import org.moqui.context.ExecutionContext
1987
1988 ExecutionContext ec = context.ec
1989
1990 ec.logger.info("Prompt requested: ${name}, sessionId: ${sessionId}")
1991
1992 // For now, return not found - can be extended later
1993 result = [error: "Prompt not found: ${name}"]
1994 ]]></script>
1995 </actions>
1996 </service>
1997
1998 <service verb="mcp" noun="RootsList" authenticate="false" allow-remote="true" transaction-timeout="30">
1999 <description>Handle MCP roots/list request</description>
2000 <in-parameters>
2001 <parameter name="sessionId"/>
2002 </in-parameters>
2003 <out-parameters>
2004 <parameter name="result" type="Map"/>
2005 </out-parameters>
2006 <actions>
2007 <script><![CDATA[
2008 import org.moqui.context.ExecutionContext
2009
2010 ExecutionContext ec = context.ec
2011
2012 // For now, return empty roots list - can be extended later
2013 def roots = []
2014
2015 result = [roots: roots]
2016 ]]></script>
2017 </actions>
2018 </service>
2019
2020 <service verb="mcp" noun="SamplingCreateMessage" authenticate="false" allow-remote="true" transaction-timeout="30">
2021 <description>Handle MCP sampling/createMessage request</description>
2022 <in-parameters>
2023 <parameter name="sessionId"/>
2024 <parameter name="messages" type="List"><description>List of messages to sample</description></parameter>
2025 <parameter name="maxTokens" type="Integer"><description>Maximum tokens to generate</description></parameter>
2026 <parameter name="temperature" type="BigDecimal"><description>Sampling temperature</description></parameter>
2027 </in-parameters>
2028 <out-parameters>
2029 <parameter name="result" type="Map"/>
2030 </out-parameters>
2031 <actions>
2032 <script><![CDATA[
2033 import org.moqui.context.ExecutionContext
2034
2035 ExecutionContext ec = context.ec
2036
2037 ec.logger.info("Sampling createMessage requested for sessionId: ${sessionId}")
2038
2039 // For now, return not implemented - can be extended with actual LLM integration
2040 result = [error: "Sampling not implemented"]
2041 ]]></script>
2042 </actions>
2043 </service>
2044
2045 <service verb="mcp" noun="ElicitationCreate" authenticate="false" allow-remote="true" transaction-timeout="30">
2046 <description>Handle MCP elicitation/create request</description>
2047 <in-parameters>
2048 <parameter name="sessionId"/>
2049 <parameter name="prompt"><description>Prompt for elicitation</description></parameter>
2050 <parameter name="context"><description>Context for elicitation</description></parameter>
2051 </in-parameters>
2052 <out-parameters>
2053 <parameter name="result" type="Map"/>
2054 </out-parameters>
2055 <actions>
2056 <script><![CDATA[
2057 import org.moqui.context.ExecutionContext
2058
2059 ExecutionContext ec = context.ec
2060
2061 ec.logger.info("Elicitation create requested for sessionId: ${sessionId}")
2062
2063 // For now, return not implemented - can be extended later
2064 result = [error: "Elicitation not implemented"]
2065 ]]></script>
2066 </actions>
2067 </service>
2068
1885 <!-- NOTE: handle#McpRequest service removed - functionality moved to screen/webapp.xml for unified handling --> 2069 <!-- NOTE: handle#McpRequest service removed - functionality moved to screen/webapp.xml for unified handling -->
1886 2070
1887 </services> 2071 </services>
......
...@@ -56,8 +56,17 @@ class EnhancedMcpServlet extends HttpServlet { ...@@ -56,8 +56,17 @@ class EnhancedMcpServlet extends HttpServlet {
56 // No need for separate session manager - Visit entity handles persistence 56 // No need for separate session manager - Visit entity handles persistence
57 private final Map<String, Integer> sessionStates = new ConcurrentHashMap<>() 57 private final Map<String, Integer> sessionStates = new ConcurrentHashMap<>()
58 58
59 // Progress tracking for notifications/progress
60 private final Map<String, Map> sessionProgress = new ConcurrentHashMap<>()
61
62 // Message storage for notifications/message
63 private final Map<String, List<Map>> sessionMessages = new ConcurrentHashMap<>()
64
65 // Subscription tracking for notifications/subscribe and notifications/unsubscribe
66 private final Map<String, Set<String>> sessionSubscriptions = new ConcurrentHashMap<>()
67
59 // Notification queue for server-initiated notifications (for non-SSE clients) 68 // Notification queue for server-initiated notifications (for non-SSE clients)
60 private final Map<String, List<Map>> notificationQueues = new ConcurrentHashMap<>() 69 private static final Map<String, List<Map>> notificationQueues = new ConcurrentHashMap<>()
61 70
62 // Configuration parameters 71 // Configuration parameters
63 private String sseEndpoint = "/sse" 72 private String sseEndpoint = "/sse"
...@@ -398,17 +407,11 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") ...@@ -398,17 +407,11 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}")
398 407
399 try { 408 try {
400 // Send initial connection event 409 // Send initial connection event
401 def connectData = [ 410 def connectData = [
402 type: "connected", 411 version: "2.0.2",
403 sessionId: visit.visitId,
404 timestamp: System.currentTimeMillis(),
405 serverInfo: [
406 name: "Moqui MCP SSE Server",
407 version: "2.0.0",
408 protocolVersion: "2025-06-18", 412 protocolVersion: "2025-06-18",
409 architecture: "Visit-based sessions with connection registry" 413 architecture: "Visit-based sessions with connection registry"
410 ] 414 ]
411 ]
412 415
413 // Set MCP session ID header per specification BEFORE sending any data 416 // Set MCP session ID header per specification BEFORE sending any data
414 response.setHeader("Mcp-Session-Id", visit.visitId.toString()) 417 response.setHeader("Mcp-Session-Id", visit.visitId.toString())
...@@ -630,6 +633,19 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") ...@@ -630,6 +633,19 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}")
630 633
631 logger.info("Enhanced MCP JSON-RPC Request: ${method} ${request.requestURI} - Accept: ${acceptHeader}, Content-Type: ${contentType}") 634 logger.info("Enhanced MCP JSON-RPC Request: ${method} ${request.requestURI} - Accept: ${acceptHeader}, Content-Type: ${contentType}")
632 635
636 // Validate Accept header per MCP 2025-11-25 spec requirement #2
637 // Client MUST include Accept header listing both application/json and text/event-stream
638 if (!acceptHeader || !(acceptHeader.contains("application/json") || acceptHeader.contains("text/event-stream"))) {
639 response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
640 response.setContentType("application/json")
641 response.writer.write(JsonOutput.toJson([
642 jsonrpc: "2.0",
643 error: [code: -32600, message: "Accept header must include application/json and/or text/event-stream per MCP 2025-11-25 spec"],
644 id: null
645 ]))
646 return
647 }
648
633 if (!"POST".equals(method)) { 649 if (!"POST".equals(method)) {
634 response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED) 650 response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED)
635 response.setContentType("application/json") 651 response.setContentType("application/json")
...@@ -710,12 +726,14 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") ...@@ -710,12 +726,14 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}")
710 726
711 // Validate MCP protocol version per specification 727 // Validate MCP protocol version per specification
712 String protocolVersion = request.getHeader("MCP-Protocol-Version") 728 String protocolVersion = request.getHeader("MCP-Protocol-Version")
713 if (protocolVersion && protocolVersion != "2025-06-18") { 729 // Support multiple protocol versions with version negotiation
730 def supportedVersions = ["2025-06-18", "2025-11-25", "2024-11-05", "2024-10-07", "2023-06-05"]
731 if (protocolVersion && !supportedVersions.contains(protocolVersion)) {
714 response.setStatus(HttpServletResponse.SC_BAD_REQUEST) 732 response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
715 response.setContentType("application/json") 733 response.setContentType("application/json")
716 response.writer.write(JsonOutput.toJson([ 734 response.writer.write(JsonOutput.toJson([
717 jsonrpc: "2.0", 735 jsonrpc: "2.0",
718 error: [code: -32600, message: "Unsupported MCP protocol version: ${protocolVersion}. Supported: 2025-06-18"], 736 error: [code: -32600, message: "Unsupported MCP protocol version: ${protocolVersion}. Supported: ${supportedVersions.join(', ')}"],
719 id: null 737 id: null
720 ])) 738 ]))
721 return 739 return
...@@ -793,31 +811,42 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") ...@@ -793,31 +811,42 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}")
793 } 811 }
794 } 812 }
795 813
796 // Process MCP method using Moqui services with session ID if available
797 def result = processMcpMethod(rpcRequest.method, rpcRequest.params, ec, sessionId, visit ?: [:])
798
799 // Check if this is a notification (no id) - notifications get empty response 814 // Check if this is a notification (no id) - notifications get empty response
800 boolean isNotification = !rpcRequest.containsKey('id') 815 boolean isNotification = !rpcRequest.containsKey('id')
801 816
802 if (isNotification) { 817 if (isNotification) {
803 // For notifications, set session header if needed and return empty JSON-RPC response 818 // Special handling for notifications/initialized to transition session state
804 if (result?.sessionId) { 819 if ("notifications/initialized".equals(rpcRequest.method)) {
805 response.setHeader("Mcp-Session-Id", result.sessionId) 820 logger.info("Processing notifications/initialized for sessionId: ${sessionId}")
821 if (sessionId) {
822 sessionStates.put(sessionId, STATE_INITIALIZED)
823 logger.info("Session ${sessionId} transitioned to INITIALIZED state")
824 }
825
826 // For notifications/initialized, return 202 Accepted per MCP HTTP Streaming spec
827 if (sessionId) {
828 response.setHeader("Mcp-Session-Id", sessionId.toString())
829 }
830 response.setStatus(HttpServletResponse.SC_ACCEPTED) // 202 Accepted
831 logger.info("Sent 202 Accepted response for notifications/initialized")
832 response.flushBuffer() // Commit the response immediately
833 return
806 } 834 }
807 835
808 // Return empty JSON-RPC response for notifications 836 // For other notifications, set session header if needed but NO response per MCP spec
809 def rpcResponse = [ 837 if (sessionId) {
810 jsonrpc: "2.0", 838 response.setHeader("Mcp-Session-Id", sessionId.toString())
811 id: rpcRequest.id, 839 }
812 result: null
813 ]
814 840
815 response.setContentType("application/json") 841 // Other notifications receive NO response per MCP specification
816 response.setCharacterEncoding("UTF-8") 842 response.setStatus(HttpServletResponse.SC_NO_CONTENT) // 204 No Content
817 response.writer.write(JsonOutput.toJson(rpcResponse)) 843 response.flushBuffer() // Commit the response immediately
818 return 844 return
819 } 845 }
820 846
847 // Process MCP method using Moqui services with session ID if available
848 def result = processMcpMethod(rpcRequest.method, rpcRequest.params, ec, sessionId, visit ?: [:])
849
821 // Set Mcp-Session-Id header BEFORE any response data (per MCP 2025-06-18 spec) 850 // Set Mcp-Session-Id header BEFORE any response data (per MCP 2025-06-18 spec)
822 // For initialize method, always use sessionId we have (from visit or header) 851 // For initialize method, always use sessionId we have (from visit or header)
823 if (rpcRequest.method == "initialize" && sessionId) { 852 if (rpcRequest.method == "initialize" && sessionId) {
...@@ -869,7 +898,7 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") ...@@ -869,7 +898,7 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}")
869 Integer sessionState = sessionId ? sessionStates.get(sessionId) : null 898 Integer sessionState = sessionId ? sessionStates.get(sessionId) : null
870 899
871 // Methods that don't require initialized session 900 // Methods that don't require initialized session
872 if (!["initialize", "ping", "notifications/initialized"].contains(method)) { 901 if (!["initialize", "ping"].contains(method)) {
873 if (sessionState != STATE_INITIALIZED) { 902 if (sessionState != STATE_INITIALIZED) {
874 logger.warn("Method ${method} called but session ${sessionId} not initialized (state: ${sessionState})") 903 logger.warn("Method ${method} called but session ${sessionId} not initialized (state: ${sessionState})")
875 return [error: "Session not initialized. Call initialize first, then send notifications/initialized."] 904 return [error: "Session not initialized. Call initialize first, then send notifications/initialized."]
...@@ -899,22 +928,35 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") ...@@ -899,22 +928,35 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}")
899 // Simple ping for testing - bypass service for now 928 // Simple ping for testing - bypass service for now
900 return [pong: System.currentTimeMillis(), sessionId: visit?.visitId, user: ec.user.username] 929 return [pong: System.currentTimeMillis(), sessionId: visit?.visitId, user: ec.user.username]
901 case "tools/list": 930 case "tools/list":
931 // Ensure sessionId is available to service for notification consistency
932 if (sessionId) params.sessionId = sessionId
902 return callMcpService("mcp#ToolsList", params, ec) 933 return callMcpService("mcp#ToolsList", params, ec)
903 case "tools/call": 934 case "tools/call":
935 // Ensure sessionId is available to service for notification consistency
936 if (sessionId) params.sessionId = sessionId
904 return callMcpService("mcp#ToolsCall", params, ec) 937 return callMcpService("mcp#ToolsCall", params, ec)
905 case "resources/list": 938 case "resources/list":
906 return callMcpService("mcp#ResourcesList", params, ec) 939 return callMcpService("mcp#ResourcesList", params, ec)
907 case "resources/read": 940 case "resources/read":
908 return callMcpService("mcp#ResourcesRead", params, ec) 941 return callMcpService("mcp#ResourcesRead", params, ec)
909 case "notifications/initialized": 942 case "resources/templates/list":
910 // Process notifications/initialized - transition session to initialized state 943 return callMcpService("mcp#ResourcesTemplatesList", params, ec)
911 // Use the header sessionId for consistency 944 case "resources/subscribe":
912 logger.info("Processing notifications/initialized for sessionId: ${sessionId}") 945 return callMcpService("mcp#ResourcesSubscribe", params, ec)
913 if (sessionId) { 946 case "resources/unsubscribe":
914 sessionStates.put(sessionId, STATE_INITIALIZED) 947 return callMcpService("mcp#ResourcesUnsubscribe", params, ec)
915 logger.info("Session ${sessionId} transitioned to INITIALIZED state") 948 case "prompts/list":
916 } 949 return callMcpService("mcp#PromptsList", params, ec)
917 return null 950 case "prompts/get":
951 return callMcpService("mcp#PromptsGet", params, ec)
952 case "roots/list":
953 return callMcpService("mcp#RootsList", params, ec)
954 case "sampling/createMessage":
955 return callMcpService("mcp#SamplingCreateMessage", params, ec)
956 case "elicitation/create":
957 return callMcpService("mcp#ElicitationCreate", params, ec)
958 // NOTE: notifications/initialized is handled as a notification, not a request method
959 // It will be processed by the notification handling logic above (lines 824-837)
918 case "notifications/tools/list_changed": 960 case "notifications/tools/list_changed":
919 // Handle tools list changed notification 961 // Handle tools list changed notification
920 logger.info("Tools list changed for sessionId: ${sessionId}") 962 logger.info("Tools list changed for sessionId: ${sessionId}")
...@@ -926,14 +968,101 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") ...@@ -926,14 +968,101 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}")
926 // Could trigger cache invalidation here if needed 968 // Could trigger cache invalidation here if needed
927 return null 969 return null
928 case "notifications/send": 970 case "notifications/send":
929 // Handle notification sending - return success for now 971 // Handle notification sending
930 return [sent: true, sessionId: sessionId] 972 def notificationMethod = params?.method
973 def notificationParams = params?.params
974 if (!notificationMethod) {
975 throw new IllegalArgumentException("method is required for sending notification")
976 }
977
978 logger.info("Sending notification ${notificationMethod} for sessionId: ${sessionId}")
979
980 // Queue notification for delivery through SSE or polling
981 if (sessionId) {
982 def notification = [
983 method: notificationMethod,
984 params: notificationParams,
985 timestamp: System.currentTimeMillis()
986 ]
987
988 // Add to notification queue
989 def queue = notificationQueues.get(sessionId) ?: []
990 queue << notification
991 notificationQueues.put(sessionId, queue)
992
993 logger.info("Notification queued for session ${sessionId}: ${notificationMethod}")
994 }
995
996 return [sent: true, sessionId: sessionId, method: notificationMethod]
931 case "notifications/subscribe": 997 case "notifications/subscribe":
932 // Handle notification subscription - return success for now 998 // Handle notification subscription
933 return [subscribed: true, sessionId: sessionId] 999 def subscriptionMethod = params?.method
1000 if (!sessionId || !subscriptionMethod) {
1001 throw new IllegalArgumentException("sessionId and method are required for subscription")
1002 }
1003 def subscriptions = sessionSubscriptions.get(sessionId) ?: new HashSet<>()
1004 subscriptions.add(subscriptionMethod)
1005 sessionSubscriptions.put(sessionId, subscriptions)
1006 logger.info("Session ${sessionId} subscribed to: ${subscriptionMethod}")
1007 return [subscribed: true, sessionId: sessionId, method: subscriptionMethod]
934 case "notifications/unsubscribe": 1008 case "notifications/unsubscribe":
935 // Handle notification unsubscription - return success for now 1009 // Handle notification unsubscription
936 return [unsubscribed: true, sessionId: sessionId] 1010 def subscriptionMethod = params?.method
1011 if (!sessionId || !subscriptionMethod) {
1012 throw new IllegalArgumentException("sessionId and method are required for unsubscription")
1013 }
1014 def subscriptions = sessionSubscriptions.get(sessionId)
1015 if (subscriptions) {
1016 subscriptions.remove(subscriptionMethod)
1017 if (subscriptions.isEmpty()) {
1018 sessionSubscriptions.remove(sessionId)
1019 } else {
1020 sessionSubscriptions.put(sessionId, subscriptions)
1021 }
1022 logger.info("Session ${sessionId} unsubscribed from: ${subscriptionMethod}")
1023 }
1024 return [unsubscribed: true, sessionId: sessionId, method: subscriptionMethod]
1025 case "notifications/progress":
1026 // Handle progress notification
1027 def progressToken = params?.progressToken
1028 def progressValue = params?.progress
1029 def total = params?.total
1030 logger.info("Progress notification for sessionId: ${sessionId}, token: ${progressToken}, progress: ${progressValue}/${total}")
1031 // Store progress for potential polling
1032 if (sessionId && progressToken) {
1033 def progressKey = "${sessionId}_${progressToken}"
1034 sessionProgress.put(progressKey, [progress: progressValue, total: total, timestamp: System.currentTimeMillis()])
1035 }
1036 return null
1037 case "notifications/resources/updated":
1038 // Handle resource updated notification
1039 def uri = params?.uri
1040 logger.info("Resource updated notification for sessionId: ${sessionId}, uri: ${uri}")
1041 // Could trigger resource cache invalidation here
1042 return null
1043 case "notifications/prompts/list_changed":
1044 // Handle prompts list changed notification
1045 logger.info("Prompts list changed for sessionId: ${sessionId}")
1046 // Could trigger prompt cache invalidation here
1047 return null
1048 case "notifications/message":
1049 // Handle general message notification
1050 def level = params?.level ?: "info"
1051 def message = params?.message
1052 def data = params?.data
1053 logger.info("Message notification for sessionId: ${sessionId}, level: ${level}, message: ${message}")
1054 // Store message for potential retrieval
1055 if (sessionId) {
1056 def messages = sessionMessages.get(sessionId) ?: []
1057 messages << [level: level, message: message, data: data, timestamp: System.currentTimeMillis()]
1058 sessionMessages.put(sessionId, messages)
1059 }
1060 return null
1061 case "notifications/roots/list_changed":
1062 // Handle roots list changed notification
1063 logger.info("Roots list changed for sessionId: ${sessionId}")
1064 // Could trigger roots cache invalidation here
1065 return null
937 case "logging/setLevel": 1066 case "logging/setLevel":
938 // Handle logging level change notification 1067 // Handle logging level change notification
939 logger.info("Logging level change requested for sessionId: ${sessionId}") 1068 logger.info("Logging level change requested for sessionId: ${sessionId}")
...@@ -1009,7 +1138,7 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") ...@@ -1009,7 +1138,7 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}")
1009 * Queue a server notification for delivery to client 1138 * Queue a server notification for delivery to client
1010 */ 1139 */
1011 void queueNotification(String sessionId, Map notification) { 1140 void queueNotification(String sessionId, Map notification) {
1012 if (!sessionId) return 1141 if (!sessionId || !notification) return
1013 1142
1014 def queue = notificationQueues.computeIfAbsent(sessionId) { [] } 1143 def queue = notificationQueues.computeIfAbsent(sessionId) { [] }
1015 queue << notification 1144 queue << notification
......
...@@ -51,7 +51,7 @@ class VisitBasedMcpSession implements MoquiMcpTransport { ...@@ -51,7 +51,7 @@ class VisitBasedMcpSession implements MoquiMcpTransport {
51 if (!metadata.mcpSession) { 51 if (!metadata.mcpSession) {
52 // Mark this Visit as an MCP session 52 // Mark this Visit as an MCP session
53 metadata.mcpSession = true 53 metadata.mcpSession = true
54 metadata.mcpProtocolVersion = "2025-06-18" 54 metadata.mcpProtocolVersion = "2025-11-25"
55 metadata.mcpCreatedAt = System.currentTimeMillis() 55 metadata.mcpCreatedAt = System.currentTimeMillis()
56 metadata.mcpTransportType = "SSE" 56 metadata.mcpTransportType = "SSE"
57 metadata.mcpMessageCount = 0 57 metadata.mcpMessageCount = 0
......