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.
Showing
3 changed files
with
359 additions
and
46 deletions
| ... | @@ -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 | ... | ... |
-
Please register or sign in to post a comment