diff --git a/src/main/java/com/pandol365/dewey/api/controller/McpController.java b/src/main/java/com/pandol365/dewey/api/controller/McpController.java index 9ec22ca..0d222a2 100644 --- a/src/main/java/com/pandol365/dewey/api/controller/McpController.java +++ b/src/main/java/com/pandol365/dewey/api/controller/McpController.java @@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.HashMap; import java.util.Map; /** @@ -23,6 +24,33 @@ public class McpController { private final McpService mcpService; + /** + * 헬스 체크 엔드포인트 (GET 요청 처리) + * Cursor나 다른 클라이언트가 서버 상태를 확인하기 위해 사용 + */ + @GetMapping(produces = {"application/json", "text/plain", "*/*"}) + public ResponseEntity> healthCheck() { + log.info("헬스 체크 요청 수신"); + Map response = new HashMap<>(); + response.put("status", "ok"); + response.put("service", "Dewey MCP Server"); + response.put("version", "0.0.1-SNAPSHOT"); + response.put("protocol", "JSON-RPC 2.0"); + response.put("endpoint", "/mcp"); + return ResponseEntity.ok() + .header("Content-Type", "application/json") + .body(response); + } + + /** + * OPTIONS 요청 처리 (CORS preflight) + */ + @RequestMapping(method = RequestMethod.OPTIONS) + public ResponseEntity options() { + log.info("OPTIONS 요청 수신 (CORS preflight)"); + return ResponseEntity.ok().build(); + } + /** * MCP JSON-RPC 2.0 엔드포인트 * 모든 MCP 요청을 처리하는 통합 엔드포인트 diff --git a/src/main/java/com/pandol365/dewey/exception/GlobalExceptionHandler.java b/src/main/java/com/pandol365/dewey/exception/GlobalExceptionHandler.java index d032f0d..007616f 100644 --- a/src/main/java/com/pandol365/dewey/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/pandol365/dewey/exception/GlobalExceptionHandler.java @@ -4,7 +4,10 @@ import com.pandol365.dewey.api.dto.response.McpJsonRpcResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.FieldError; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -19,6 +22,72 @@ import java.util.Map; @RestControllerAdvice public class GlobalExceptionHandler { + /** + * HTTP 메서드가 지원되지 않는 경우 처리 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity> handleMethodNotSupported( + HttpRequestMethodNotSupportedException ex) { + log.warn("지원되지 않는 HTTP 메서드: {}", ex.getMethod()); + + Map response = new HashMap<>(); + response.put("error", "Method not allowed"); + response.put("message", ex.getMessage()); + if (ex.getSupportedMethods() != null) { + response.put("supportedMethods", ex.getSupportedMethods()); + } + + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(response); + } + + /** + * JSON 파싱 오류 처리 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleJsonParseException( + HttpMessageNotReadableException ex) { + log.warn("JSON 파싱 오류: {}", ex.getMessage()); + + // 원인 예외 메시지 추출 + String errorMessage = "Invalid JSON format"; + if (ex.getCause() != null) { + errorMessage = ex.getCause().getMessage(); + } + + McpJsonRpcResponse.McpError error = McpJsonRpcResponse.McpError.builder() + .code(-32700) // Parse error + .message(errorMessage) + .data(null) + .build(); + + McpJsonRpcResponse response = McpJsonRpcResponse.builder() + .jsonrpc("2.0") + .error(error) + .build(); + + return ResponseEntity.badRequest().body(response); + } + + /** + * 미디어 타입이 허용되지 않는 경우 처리 + */ + @ExceptionHandler(HttpMediaTypeNotAcceptableException.class) + public ResponseEntity> handleMediaTypeNotAcceptable( + HttpMediaTypeNotAcceptableException ex) { + log.warn("허용되지 않는 미디어 타입: {}", ex.getMessage()); + + Map response = new HashMap<>(); + response.put("status", "ok"); + response.put("service", "Dewey MCP Server"); + response.put("version", "0.0.1-SNAPSHOT"); + response.put("protocol", "JSON-RPC 2.0"); + response.put("endpoint", "/mcp"); + + return ResponseEntity.ok() + .header("Content-Type", "application/json") + .body(response); + } + /** * 유효성 검증 예외 처리 */ diff --git a/test_mcp.json b/test_mcp.json new file mode 100644 index 0000000..34ca7af --- /dev/null +++ b/test_mcp.json @@ -0,0 +1,14 @@ +{ + "jsonrpc": "2.0", + "id": "1", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } +} + diff --git a/test_store_memory.json b/test_store_memory.json new file mode 100644 index 0000000..9d9a00f --- /dev/null +++ b/test_store_memory.json @@ -0,0 +1,16 @@ +{ + "jsonrpc": "2.0", + "id": "3", + "method": "tools/call", + "params": { + "arguments": { + "name": "store_memory", + "arguments": { + "user_id": "test_user", + "memory_text": "테스트 메모리입니다", + "importance": 3 + } + } + } +} + diff --git a/test_tools_list.json b/test_tools_list.json new file mode 100644 index 0000000..ac2359b --- /dev/null +++ b/test_tools_list.json @@ -0,0 +1,6 @@ +{ + "jsonrpc": "2.0", + "id": "2", + "method": "tools/list" +} +