Continued effort to rough in a notifications bus
Showing
1 changed file
with
113 additions
and
66 deletions
| ... | @@ -312,71 +312,104 @@ try { | ... | @@ -312,71 +312,104 @@ try { |
| 312 | private void handleSseConnection(HttpServletRequest request, HttpServletResponse response, ExecutionContextImpl ec, String webappName) | 312 | private void handleSseConnection(HttpServletRequest request, HttpServletResponse response, ExecutionContextImpl ec, String webappName) |
| 313 | throws IOException { | 313 | throws IOException { |
| 314 | 314 | ||
| 315 | logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | 315 | logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 316 | |||
| 317 | // Initialize web facade for Visit creation, but avoid screen resolution | ||
| 318 | // Modify request path to avoid ScreenResourceNotFoundException | ||
| 319 | String originalRequestURI = request.getRequestURI() | ||
| 320 | String originalPathInfo = request.getPathInfo() | ||
| 321 | request.setAttribute("javax.servlet.include.request_uri", "/mcp") | ||
| 322 | request.setAttribute("javax.servlet.include.path_info", "") | ||
| 323 | 316 | ||
| 317 | // Check for existing session ID first | ||
| 318 | String sessionId = request.getHeader("Mcp-Session-Id") | ||
| 324 | def visit = null | 319 | def visit = null |
| 325 | 320 | ||
| 326 | try { | 321 | // If we have a session ID, try to find existing Visit |
| 327 | ec.initWebFacade(webappName, request, response) | 322 | if (sessionId) { |
| 328 | // Web facade was successful, get the Visit it created | ||
| 329 | visit = ec.user.getVisit() | ||
| 330 | if (!visit) { | ||
| 331 | throw new Exception("Web facade succeeded but no Visit created") | ||
| 332 | } | ||
| 333 | } catch (Exception e) { | ||
| 334 | logger.warn("Web facade initialization failed: ${e.message}, trying manual Visit creation") | ||
| 335 | // Try to create Visit manually using the same pattern as UserFacadeImpl | ||
| 336 | try { | 323 | try { |
| 337 | def visitParams = [ | ||
| 338 | sessionId: request.session.id, | ||
| 339 | webappName: webappName, | ||
| 340 | fromDate: new Timestamp(System.currentTimeMillis()), | ||
| 341 | initialLocale: request.locale.toString(), | ||
| 342 | initialRequest: (request.requestURL.toString() + (request.queryString ? "?" + request.queryString : "")).take(255), | ||
| 343 | initialReferrer: request.getHeader("Referer")?.take(255), | ||
| 344 | initialUserAgent: request.getHeader("User-Agent")?.take(255), | ||
| 345 | clientHostName: request.remoteHost, | ||
| 346 | clientUser: request.remoteUser, | ||
| 347 | serverIpAddress: ec.ecfi.getLocalhostAddress().getHostAddress(), | ||
| 348 | serverHostName: ec.ecfi.getLocalhostAddress().getHostName(), | ||
| 349 | clientIpAddress: request.remoteAddr, | ||
| 350 | userId: ec.user.userId, | ||
| 351 | userCreated: "Y" | ||
| 352 | ] | ||
| 353 | |||
| 354 | logger.info("Creating Visit with params: ${visitParams}") | ||
| 355 | def visitResult = ec.service.sync().name("create", "moqui.server.Visit") | ||
| 356 | .parameters(visitParams) | ||
| 357 | .disableAuthz() | ||
| 358 | .call() | ||
| 359 | logger.info("Visit creation result: ${visitResult}") | ||
| 360 | |||
| 361 | if (!visitResult || !visitResult.visitId) { | ||
| 362 | throw new Exception("Visit creation service returned null or no visitId") | ||
| 363 | } | ||
| 364 | |||
| 365 | // Look up the actual Visit EntityValue | ||
| 366 | visit = ec.entity.find("moqui.server.Visit") | 324 | visit = ec.entity.find("moqui.server.Visit") |
| 367 | .condition("visitId", visitResult.visitId) | 325 | .condition("visitId", sessionId) |
| 368 | .disableAuthz() | 326 | .disableAuthz() |
| 369 | .one() | 327 | .one() |
| 328 | |||
| 329 | if (visit) { | ||
| 330 | // Verify user has access to this Visit | ||
| 331 | if (!visit.userId || !ec.user.userId || visit.userId.toString() != ec.user.userId.toString()) { | ||
| 332 | logger.warn("Visit userId ${visit.userId} doesn't match current user userId ${ec.user.userId} - access denied") | ||
| 333 | response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied for session: " + sessionId) | ||
| 334 | return | ||
| 335 | } | ||
| 336 | |||
| 337 | // Set existing visit ID in HTTP session | ||
| 338 | request.session.setAttribute("moqui.visitId", sessionId) | ||
| 339 | logger.info("Reusing existing Visit ${sessionId} for user ${ec.user.username}") | ||
| 340 | } else { | ||
| 341 | logger.warn("Session ID ${sessionId} not found, will create new Visit") | ||
| 342 | } | ||
| 343 | } catch (Exception e) { | ||
| 344 | logger.warn("Error looking up existing session ${sessionId}: ${e.message}") | ||
| 345 | } | ||
| 346 | } | ||
| 347 | |||
| 348 | // Only create new Visit if we didn't find an existing one | ||
| 349 | if (!visit) { | ||
| 350 | // Initialize web facade for Visit creation, but avoid screen resolution | ||
| 351 | // Modify request path to avoid ScreenResourceNotFoundException | ||
| 352 | String originalRequestURI = request.getRequestURI() | ||
| 353 | String originalPathInfo = request.getPathInfo() | ||
| 354 | request.setAttribute("javax.servlet.include.request_uri", "/mcp") | ||
| 355 | request.setAttribute("javax.servlet.include.path_info", "") | ||
| 356 | |||
| 357 | try { | ||
| 358 | ec.initWebFacade(webappName, request, response) | ||
| 359 | // Web facade was successful, get the Visit it created | ||
| 360 | visit = ec.user.getVisit() | ||
| 370 | if (!visit) { | 361 | if (!visit) { |
| 371 | throw new Exception("Failed to look up newly created Visit") | 362 | throw new Exception("Web facade succeeded but no Visit created") |
| 363 | } | ||
| 364 | logger.info("Created new Visit ${visit.visitId} for user ${ec.user.username}") | ||
| 365 | } catch (Exception e) { | ||
| 366 | logger.warn("Web facade initialization failed: ${e.message}, trying manual Visit creation") | ||
| 367 | // Try to create Visit manually using the same pattern as UserFacadeImpl | ||
| 368 | try { | ||
| 369 | def visitParams = [ | ||
| 370 | sessionId: request.session.id, | ||
| 371 | webappName: webappName, | ||
| 372 | fromDate: new Timestamp(System.currentTimeMillis()), | ||
| 373 | initialLocale: request.locale.toString(), | ||
| 374 | initialRequest: (request.requestURL.toString() + (request.queryString ? "?" + request.queryString : "")).take(255), | ||
| 375 | initialReferrer: request.getHeader("Referer")?.take(255), | ||
| 376 | initialUserAgent: request.getHeader("User-Agent")?.take(255), | ||
| 377 | clientHostName: request.remoteHost, | ||
| 378 | clientUser: request.remoteUser, | ||
| 379 | serverIpAddress: ec.ecfi.getLocalhostAddress().getHostAddress(), | ||
| 380 | serverHostName: ec.ecfi.getLocalhostAddress().getHostName(), | ||
| 381 | clientIpAddress: request.remoteAddr, | ||
| 382 | userId: ec.user.userId, | ||
| 383 | userCreated: "Y" | ||
| 384 | ] | ||
| 385 | |||
| 386 | logger.info("Creating Visit with params: ${visitParams}") | ||
| 387 | def visitResult = ec.service.sync().name("create", "moqui.server.Visit") | ||
| 388 | .parameters(visitParams) | ||
| 389 | .disableAuthz() | ||
| 390 | .call() | ||
| 391 | logger.info("Visit creation result: ${visitResult}") | ||
| 392 | |||
| 393 | if (!visitResult || !visitResult.visitId) { | ||
| 394 | throw new Exception("Visit creation service returned null or no visitId") | ||
| 395 | } | ||
| 396 | |||
| 397 | // Look up the actual Visit EntityValue | ||
| 398 | visit = ec.entity.find("moqui.server.Visit") | ||
| 399 | .condition("visitId", visitResult.visitId) | ||
| 400 | .disableAuthz() | ||
| 401 | .one() | ||
| 402 | if (!visit) { | ||
| 403 | throw new Exception("Failed to look up newly created Visit") | ||
| 404 | } | ||
| 405 | ec.web.session.setAttribute("moqui.visitId", visit.visitId) | ||
| 406 | logger.info("Manually created Visit ${visit.visitId} for user ${ec.user.username}") | ||
| 407 | |||
| 408 | } catch (Exception visitEx) { | ||
| 409 | logger.error("Manual Visit creation failed: ${visitEx.message}", visitEx) | ||
| 410 | response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to create Visit") | ||
| 411 | return | ||
| 372 | } | 412 | } |
| 373 | ec.web.session.setAttribute("moqui.visitId", visit.visitId) | ||
| 374 | logger.info("Manually created Visit ${visit.visitId} for user ${ec.user.username}") | ||
| 375 | |||
| 376 | } catch (Exception visitEx) { | ||
| 377 | logger.error("Manual Visit creation failed: ${visitEx.message}", visitEx) | ||
| 378 | response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to create Visit") | ||
| 379 | return | ||
| 380 | } | 413 | } |
| 381 | } | 414 | } |
| 382 | 415 | ||
| ... | @@ -406,7 +439,14 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -406,7 +439,14 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 406 | VisitBasedMcpSession session = new VisitBasedMcpSession(visit, response.writer, ec) | 439 | VisitBasedMcpSession session = new VisitBasedMcpSession(visit, response.writer, ec) |
| 407 | 440 | ||
| 408 | try { | 441 | try { |
| 409 | // Send initial connection event | 442 | // Check if this is old HTTP+SSE transport (no session ID, no prior initialization) |
| 443 | // Send endpoint event first for backwards compatibility | ||
| 444 | if (!request.getHeader("Mcp-Session-Id")) { | ||
| 445 | logger.info("No Mcp-Session-Id header detected, assuming old HTTP+SSE transport") | ||
| 446 | sendSseEvent(response.writer, "endpoint", "/mcp", 0) | ||
| 447 | } | ||
| 448 | |||
| 449 | // Send initial connection event for new transport | ||
| 410 | def connectData = [ | 450 | def connectData = [ |
| 411 | version: "2.0.2", | 451 | version: "2.0.2", |
| 412 | protocolVersion: "2025-06-18", | 452 | protocolVersion: "2025-06-18", |
| ... | @@ -415,11 +455,9 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -415,11 +455,9 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 415 | 455 | ||
| 416 | // Set MCP session ID header per specification BEFORE sending any data | 456 | // Set MCP session ID header per specification BEFORE sending any data |
| 417 | response.setHeader("Mcp-Session-Id", visit.visitId.toString()) | 457 | response.setHeader("Mcp-Session-Id", visit.visitId.toString()) |
| 458 | logger.info("Set Mcp-Session-Id header to ${visit.visitId} for SSE connection") | ||
| 418 | 459 | ||
| 419 | sendSseEvent(response.writer, "connect", JsonOutput.toJson(connectData), 0) | 460 | sendSseEvent(response.writer, "connect", JsonOutput.toJson(connectData), 1) |
| 420 | |||
| 421 | // Send endpoint info for message posting (for compatibility) | ||
| 422 | sendSseEvent(response.writer, "endpoint", "/mcp", 1) | ||
| 423 | 461 | ||
| 424 | // Keep connection alive with periodic pings | 462 | // Keep connection alive with periodic pings |
| 425 | int pingCount = 0 | 463 | int pingCount = 0 |
| ... | @@ -849,10 +887,19 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -849,10 +887,19 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 849 | 887 | ||
| 850 | // Set Mcp-Session-Id header BEFORE any response data (per MCP 2025-06-18 spec) | 888 | // Set Mcp-Session-Id header BEFORE any response data (per MCP 2025-06-18 spec) |
| 851 | // For initialize method, always use sessionId we have (from visit or header) | 889 | // For initialize method, always use sessionId we have (from visit or header) |
| 890 | String responseSessionId = null | ||
| 852 | if (rpcRequest.method == "initialize" && sessionId) { | 891 | if (rpcRequest.method == "initialize" && sessionId) { |
| 853 | response.setHeader("Mcp-Session-Id", sessionId.toString()) | 892 | responseSessionId = sessionId.toString() |
| 854 | } else if (result?.sessionId) { | 893 | } else if (result?.sessionId) { |
| 855 | response.setHeader("Mcp-Session-Id", result.sessionId.toString()) | 894 | responseSessionId = result.sessionId.toString() |
| 895 | } else if (sessionId) { | ||
| 896 | // For other methods, ensure we always return the session ID from header | ||
| 897 | responseSessionId = sessionId.toString() | ||
| 898 | } | ||
| 899 | |||
| 900 | if (responseSessionId) { | ||
| 901 | response.setHeader("Mcp-Session-Id", responseSessionId) | ||
| 902 | logger.info("Set Mcp-Session-Id header to ${responseSessionId} for method ${rpcRequest.method}") | ||
| 856 | } | 903 | } |
| 857 | 904 | ||
| 858 | // Build JSON-RPC response for regular requests | 905 | // Build JSON-RPC response for regular requests |
| ... | @@ -910,9 +957,9 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | ... | @@ -910,9 +957,9 @@ logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 910 | // For initialize, use the visitId we just created instead of null sessionId from request | 957 | // For initialize, use the visitId we just created instead of null sessionId from request |
| 911 | if (visit && visit.visitId) { | 958 | if (visit && visit.visitId) { |
| 912 | params.sessionId = visit.visitId | 959 | params.sessionId = visit.visitId |
| 913 | // Set session to initializing state using the header sessionId as key (for consistency) | 960 | // Set session to initializing state using actual sessionId as key (for consistency) |
| 914 | sessionStates.put(sessionId, STATE_INITIALIZING) | 961 | sessionStates.put(params.sessionId, STATE_INITIALIZING) |
| 915 | logger.info("Initialize - using visitId: ${visit.visitId}, set state ${sessionId} to INITIALIZING") | 962 | logger.info("Initialize - using visitId: ${visit.visitId}, set state ${params.sessionId} to INITIALIZING") |
| 916 | } else { | 963 | } else { |
| 917 | logger.warn("Initialize - no visit available, using null sessionId") | 964 | logger.warn("Initialize - no visit available, using null sessionId") |
| 918 | } | 965 | } | ... | ... |
-
Please register or sign in to post a comment