88 lines
3.8 KiB
Java
88 lines
3.8 KiB
Java
package com.cloudhandson.ddsbackoffice.web;
|
|
|
|
import com.cloudhandson.ddsbackoffice.domain.DdsMcpAuthenticatedUser;
|
|
import com.cloudhandson.ddsbackoffice.service.DdsMcpBearerAuthenticator;
|
|
import com.cloudhandson.ddsbackoffice.service.DdsMcpSseService;
|
|
import com.cloudhandson.vpdbackoffice.service.AppException;
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
import java.io.IOException;
|
|
import java.util.Map;
|
|
import java.util.UUID;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import org.springframework.http.MediaType;
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.stereotype.Controller;
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
import org.springframework.web.bind.annotation.RequestBody;
|
|
import org.springframework.web.bind.annotation.RequestHeader;
|
|
import org.springframework.web.bind.annotation.RequestParam;
|
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
|
|
/**
|
|
* DDS-specific SSE transport. Authorization is deliberately rechecked for
|
|
* every message rather than being trusted for the life of an SSE connection.
|
|
*/
|
|
@Controller
|
|
public class DdsMcpSseController {
|
|
|
|
private static final long SSE_TIMEOUT_MILLIS = 30L * 60L * 1000L;
|
|
|
|
private final DdsMcpBearerAuthenticator bearerAuthenticator;
|
|
private final DdsMcpSseService service;
|
|
private final Map<String, Session> sessions = new ConcurrentHashMap<>();
|
|
|
|
public DdsMcpSseController(DdsMcpBearerAuthenticator bearerAuthenticator, DdsMcpSseService service) {
|
|
this.bearerAuthenticator = bearerAuthenticator;
|
|
this.service = service;
|
|
}
|
|
|
|
@GetMapping(path = {"/dds/mcp/sse", "/mcp/dds/sse"}, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
|
public SseEmitter sse(
|
|
@RequestHeader(name = "Authorization", required = false) String authorization,
|
|
HttpServletRequest request
|
|
) throws IOException {
|
|
DdsMcpAuthenticatedUser user = bearerAuthenticator.authenticate(authorization);
|
|
String sessionId = UUID.randomUUID().toString();
|
|
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT_MILLIS);
|
|
sessions.put(sessionId, new Session(user.applicationUserId(), emitter));
|
|
emitter.onCompletion(() -> sessions.remove(sessionId));
|
|
emitter.onTimeout(() -> sessions.remove(sessionId));
|
|
emitter.onError(error -> sessions.remove(sessionId));
|
|
String messagesPath = request.getRequestURI().startsWith("/mcp/dds/")
|
|
? "/mcp/dds/messages"
|
|
: "/dds/mcp/messages";
|
|
emitter.send(SseEmitter.event().name("endpoint").data(messagesPath + "?sessionId=" + sessionId));
|
|
return emitter;
|
|
}
|
|
|
|
@PostMapping(path = {"/dds/mcp/messages", "/mcp/dds/messages"}, consumes = MediaType.APPLICATION_JSON_VALUE)
|
|
public ResponseEntity<?> message(
|
|
@RequestHeader(name = "Authorization", required = false) String authorization,
|
|
@RequestParam(required = false) String sessionId,
|
|
@RequestBody JsonNode request
|
|
) throws IOException {
|
|
DdsMcpAuthenticatedUser user = bearerAuthenticator.authenticate(authorization);
|
|
ObjectNode response = service.handle(user, request);
|
|
if (sessionId == null || sessionId.isBlank()) {
|
|
return ResponseEntity.ok(response);
|
|
}
|
|
Session session = sessions.get(sessionId);
|
|
if (session == null || session.applicationUserId() != user.applicationUserId()) {
|
|
throw new AppException("AUTHORIZATION_DENIED: MCP 세션을 확인할 수 없습니다.");
|
|
}
|
|
try {
|
|
session.emitter().send(SseEmitter.event().name("message").data(response));
|
|
return ResponseEntity.accepted().build();
|
|
} catch (IOException exception) {
|
|
sessions.remove(sessionId);
|
|
throw exception;
|
|
}
|
|
}
|
|
|
|
private record Session(long applicationUserId, SseEmitter emitter) {
|
|
}
|
|
}
|