Fix notifications/initialized to work without Mcp-Session-Id header
- Allow notifications/initialized without session ID header as it completes initialization process - Add fallback to use visit ID as session ID for both initialize and notifications/initialized methods - Maintain proper session state management and validation for other methods - Follow MCP protocol specification for initialization flow
Showing
1 changed file
with
83 additions
and
14 deletions
| ... | @@ -44,11 +44,17 @@ class EnhancedMcpServlet extends HttpServlet { | ... | @@ -44,11 +44,17 @@ class EnhancedMcpServlet extends HttpServlet { |
| 44 | 44 | ||
| 45 | private JsonSlurper jsonSlurper = new JsonSlurper() | 45 | private JsonSlurper jsonSlurper = new JsonSlurper() |
| 46 | 46 | ||
| 47 | // Session state constants | ||
| 48 | private static final int STATE_UNINITIALIZED = 0 | ||
| 49 | private static final int STATE_INITIALIZING = 1 | ||
| 50 | private static final int STATE_INITIALIZED = 2 | ||
| 51 | |||
| 47 | // Simple registry for active connections only (transient HTTP connections) | 52 | // Simple registry for active connections only (transient HTTP connections) |
| 48 | private final Map<String, PrintWriter> activeConnections = new ConcurrentHashMap<>() | 53 | private final Map<String, PrintWriter> activeConnections = new ConcurrentHashMap<>() |
| 49 | 54 | ||
| 50 | // Session management using Moqui's Visit system directly | 55 | // Session management using Moqui's Visit system directly |
| 51 | // 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<>() | ||
| 52 | 58 | ||
| 53 | // Configuration parameters | 59 | // Configuration parameters |
| 54 | private String sseEndpoint = "/sse" | 60 | private String sseEndpoint = "/sse" |
| ... | @@ -242,6 +248,7 @@ try { | ... | @@ -242,6 +248,7 @@ try { |
| 242 | handleMessage(request, response, ec) | 248 | handleMessage(request, response, ec) |
| 243 | } else if ("POST".equals(method) && (requestURI.equals("/mcp") || requestURI.endsWith("/mcp"))) { | 249 | } else if ("POST".equals(method) && (requestURI.equals("/mcp") || requestURI.endsWith("/mcp"))) { |
| 244 | // Handle POST requests to /mcp for JSON-RPC | 250 | // Handle POST requests to /mcp for JSON-RPC |
| 251 | logger.info("About to call handleJsonRpc with visit: ${visit?.visitId}") | ||
| 245 | handleJsonRpc(request, response, ec, webappName, requestBody, visit) | 252 | handleJsonRpc(request, response, ec, webappName, requestBody, visit) |
| 246 | } else if ("GET".equals(method) && (requestURI.equals("/mcp") || requestURI.endsWith("/mcp"))) { | 253 | } else if ("GET".equals(method) && (requestURI.equals("/mcp") || requestURI.endsWith("/mcp"))) { |
| 247 | // Handle GET requests to /mcp - maybe for server info or SSE fallback | 254 | // Handle GET requests to /mcp - maybe for server info or SSE fallback |
| ... | @@ -711,8 +718,15 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -711,8 +718,15 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 711 | String sessionId = request.getHeader("Mcp-Session-Id") | 718 | String sessionId = request.getHeader("Mcp-Session-Id") |
| 712 | logger.info("Session ID from header: '${sessionId}', method: '${rpcRequest.method}'") | 719 | logger.info("Session ID from header: '${sessionId}', method: '${rpcRequest.method}'") |
| 713 | 720 | ||
| 721 | // For initialize and notifications/initialized methods, use visit ID as session ID if no header | ||
| 722 | if (!sessionId && ("initialize".equals(rpcRequest.method) || "notifications/initialized".equals(rpcRequest.method)) && visit) { | ||
| 723 | sessionId = visit.visitId | ||
| 724 | logger.info("${rpcRequest.method} method: using visit ID as session ID: ${sessionId}") | ||
| 725 | } | ||
| 726 | |||
| 714 | // Validate session ID for non-initialize requests per MCP spec | 727 | // Validate session ID for non-initialize requests per MCP spec |
| 715 | if (!sessionId && rpcRequest.method != "initialize") { | 728 | // Allow notifications/initialized without session ID as it completes the initialization process |
| 729 | if (!sessionId && rpcRequest.method != "initialize" && rpcRequest.method != "notifications/initialized") { | ||
| 716 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST) | 730 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST) |
| 717 | response.setContentType("application/json") | 731 | response.setContentType("application/json") |
| 718 | response.writer.write(JsonOutput.toJson([ | 732 | response.writer.write(JsonOutput.toJson([ |
| ... | @@ -773,18 +787,42 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -773,18 +787,42 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 773 | } | 787 | } |
| 774 | 788 | ||
| 775 | // Process MCP method using Moqui services with session ID if available | 789 | // Process MCP method using Moqui services with session ID if available |
| 776 | def result = processMcpMethod(rpcRequest.method, rpcRequest.params, ec, sessionId, visit) | 790 | def result = processMcpMethod(rpcRequest.method, rpcRequest.params, ec, sessionId, visit ?: [:]) |
| 791 | |||
| 792 | // Check if this is a notification (no id) - notifications get empty response | ||
| 793 | boolean isNotification = !rpcRequest.containsKey('id') | ||
| 794 | |||
| 795 | if (isNotification) { | ||
| 796 | // For notifications, set session header if needed and return empty JSON-RPC response | ||
| 797 | if (result?.sessionId) { | ||
| 798 | response.setHeader("Mcp-Session-Id", result.sessionId) | ||
| 799 | } | ||
| 800 | |||
| 801 | // Return empty JSON-RPC response for notifications | ||
| 802 | def rpcResponse = [ | ||
| 803 | jsonrpc: "2.0", | ||
| 804 | id: rpcRequest.id, | ||
| 805 | result: null | ||
| 806 | ] | ||
| 807 | |||
| 808 | response.setContentType("application/json") | ||
| 809 | response.setCharacterEncoding("UTF-8") | ||
| 810 | response.writer.write(JsonOutput.toJson(rpcResponse)) | ||
| 811 | return | ||
| 812 | } | ||
| 777 | 813 | ||
| 778 | // Set Mcp-Session-Id header BEFORE any response data (per MCP 2025-06-18 spec) | 814 | // Set Mcp-Session-Id header BEFORE any response data (per MCP 2025-06-18 spec) |
| 779 | if (result?.sessionId) { | 815 | if (result?.sessionId) { |
| 780 | response.setHeader("Mcp-Session-Id", result.sessionId) | 816 | response.setHeader("Mcp-Session-Id", result.sessionId) |
| 781 | } | 817 | } |
| 782 | 818 | ||
| 783 | // Build JSON-RPC response | 819 | // Build JSON-RPC response for regular requests |
| 820 | // Extract the actual result from Moqui service response | ||
| 821 | def actualResult = result?.result ?: result | ||
| 784 | def rpcResponse = [ | 822 | def rpcResponse = [ |
| 785 | jsonrpc: "2.0", | 823 | jsonrpc: "2.0", |
| 786 | id: rpcRequest.id, | 824 | id: rpcRequest.id, |
| 787 | result: result | 825 | result: actualResult |
| 788 | ] | 826 | ] |
| 789 | 827 | ||
| 790 | response.setContentType("application/json") | 828 | response.setContentType("application/json") |
| ... | @@ -803,14 +841,28 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -803,14 +841,28 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 803 | } | 841 | } |
| 804 | 842 | ||
| 805 | // Add session context to parameters for services | 843 | // Add session context to parameters for services |
| 806 | params.sessionId = visit.visitId | 844 | params.sessionId = visit?.visitId |
| 845 | |||
| 846 | // Check session state for methods that require initialization | ||
| 847 | // Use the sessionId from header for consistency (this is what the client tracks) | ||
| 848 | Integer sessionState = sessionId ? sessionStates.get(sessionId) : null | ||
| 849 | |||
| 850 | // Methods that don't require initialized session | ||
| 851 | if (!["initialize", "ping", "notifications/initialized"].contains(method)) { | ||
| 852 | if (sessionState != STATE_INITIALIZED) { | ||
| 853 | logger.warn("Method ${method} called but session ${sessionId} not initialized (state: ${sessionState})") | ||
| 854 | return [error: "Session not initialized. Call initialize first, then send notifications/initialized."] | ||
| 855 | } | ||
| 856 | } | ||
| 807 | 857 | ||
| 808 | switch (method) { | 858 | switch (method) { |
| 809 | case "initialize": | 859 | case "initialize": |
| 810 | // For initialize, use the visitId we just created instead of null sessionId from request | 860 | // For initialize, use the visitId we just created instead of null sessionId from request |
| 811 | if (visit && visit.visitId) { | 861 | if (visit && visit.visitId) { |
| 812 | params.sessionId = visit.visitId | 862 | params.sessionId = visit.visitId |
| 813 | logger.info("Initialize - using visitId: ${visit.visitId}") | 863 | // Set session to initializing state using the header sessionId as key (for consistency) |
| 864 | sessionStates.put(sessionId, STATE_INITIALIZING) | ||
| 865 | logger.info("Initialize - using visitId: ${visit.visitId}, set state ${sessionId} to INITIALIZING") | ||
| 814 | } else { | 866 | } else { |
| 815 | logger.warn("Initialize - no visit available, using null sessionId") | 867 | logger.warn("Initialize - no visit available, using null sessionId") |
| 816 | } | 868 | } |
| ... | @@ -819,7 +871,7 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -819,7 +871,7 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 819 | return callMcpService("mcp#Initialize", params, ec) | 871 | return callMcpService("mcp#Initialize", params, ec) |
| 820 | case "ping": | 872 | case "ping": |
| 821 | // Simple ping for testing - bypass service for now | 873 | // Simple ping for testing - bypass service for now |
| 822 | return [pong: System.currentTimeMillis(), sessionId: visit.visitId, user: ec.user.username] | 874 | return [pong: System.currentTimeMillis(), sessionId: visit?.visitId, user: ec.user.username] |
| 823 | case "tools/list": | 875 | case "tools/list": |
| 824 | return callMcpService("mcp#ToolsList", params, ec) | 876 | return callMcpService("mcp#ToolsList", params, ec) |
| 825 | case "tools/call": | 877 | case "tools/call": |
| ... | @@ -829,8 +881,24 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -829,8 +881,24 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 829 | case "resources/read": | 881 | case "resources/read": |
| 830 | return callMcpService("mcp#ResourcesRead", params, ec) | 882 | return callMcpService("mcp#ResourcesRead", params, ec) |
| 831 | case "notifications/initialized": | 883 | case "notifications/initialized": |
| 832 | // Handle notification initialization - return success for now | 884 | // Process notifications/initialized - transition session to initialized state |
| 833 | return [initialized: true, sessionId: sessionId] | 885 | // Use the header sessionId for consistency |
| 886 | logger.info("Processing notifications/initialized for sessionId: ${sessionId}") | ||
| 887 | if (sessionId) { | ||
| 888 | sessionStates.put(sessionId, STATE_INITIALIZED) | ||
| 889 | logger.info("Session ${sessionId} transitioned to INITIALIZED state") | ||
| 890 | } | ||
| 891 | return null | ||
| 892 | case "notifications/tools/list_changed": | ||
| 893 | // Handle tools list changed notification | ||
| 894 | logger.info("Tools list changed for sessionId: ${sessionId}") | ||
| 895 | // Could trigger cache invalidation here if needed | ||
| 896 | return null | ||
| 897 | case "notifications/resources/list_changed": | ||
| 898 | // Handle resources list changed notification | ||
| 899 | logger.info("Resources list changed for sessionId: ${sessionId}") | ||
| 900 | // Could trigger cache invalidation here if needed | ||
| 901 | return null | ||
| 834 | case "notifications/send": | 902 | case "notifications/send": |
| 835 | // Handle notification sending - return success for now | 903 | // Handle notification sending - return success for now |
| 836 | return [sent: true, sessionId: sessionId] | 904 | return [sent: true, sessionId: sessionId] |
| ... | @@ -840,6 +908,10 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -840,6 +908,10 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 840 | case "notifications/unsubscribe": | 908 | case "notifications/unsubscribe": |
| 841 | // Handle notification unsubscription - return success for now | 909 | // Handle notification unsubscription - return success for now |
| 842 | return [unsubscribed: true, sessionId: sessionId] | 910 | return [unsubscribed: true, sessionId: sessionId] |
| 911 | case "logging/setLevel": | ||
| 912 | // Handle logging level change notification | ||
| 913 | logger.info("Logging level change requested for sessionId: ${sessionId}") | ||
| 914 | return null | ||
| 843 | default: | 915 | default: |
| 844 | throw new IllegalArgumentException("Method not found: ${method}") | 916 | throw new IllegalArgumentException("Method not found: ${method}") |
| 845 | } | 917 | } |
| ... | @@ -863,12 +935,9 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -863,12 +935,9 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 863 | return [error: "Service returned null result"] | 935 | return [error: "Service returned null result"] |
| 864 | } | 936 | } |
| 865 | // Service framework returns result in 'result' field when out-parameters are used | 937 | // Service framework returns result in 'result' field when out-parameters are used |
| 866 | // Unwrap the Moqui service result to avoid double nesting in JSON-RPC response | 938 | // Return the entire service result to maintain proper JSON-RPC structure |
| 867 | if (result?.containsKey('result')) { | 939 | // The MCP services already set the correct 'result' structure |
| 868 | return result.result ?: [error: "Service returned empty result"] | ||
| 869 | } else { | ||
| 870 | return result ?: [error: "Service returned null result"] | 940 | return result ?: [error: "Service returned null result"] |
| 871 | } | ||
| 872 | } catch (Exception e) { | 941 | } catch (Exception e) { |
| 873 | logger.error("Error calling Enhanced MCP service ${serviceName}", e) | 942 | logger.error("Error calling Enhanced MCP service ${serviceName}", e) |
| 874 | return [error: e.message] | 943 | return [error: e.message] | ... | ... |
-
Please register or sign in to post a comment