1c226c77 by Ean Schuessler

Continued effort to rough in a notifications bus

1 parent 6bbfd372
...@@ -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 }
......