Cursor MCP 연결 성공

This commit is contained in:
mskim 2025-12-10 13:45:10 +09:00
parent 29f381ae81
commit de5eede11d
4 changed files with 236 additions and 26 deletions

View File

@ -4,7 +4,7 @@
Dewey MCP (Model Context Protocol) 서버는 JSON-RPC 2.0 프로토콜을 기반으로 AI 장기기억 시스템의 기능을 제공합니다.
- **프로토콜 버전**: 2024-11-05
- **프로토콜 버전**: 2025-06-18 (클라이언트가 보낸 버전 사용, 기본값: 2024-11-05)
- **서버 이름**: Dewey Memory Server
- **서버 버전**: 0.0.1-SNAPSHOT
- **Base URL**: `http://localhost:8080/mcp`
@ -40,6 +40,44 @@ Database (PostgreSQL / Redis)
## 엔드포인트
### GET /mcp
서버 상태를 확인하는 헬스 체크 엔드포인트입니다.
**요청:**
```
GET http://localhost:8080/mcp
```
**응답:**
```json
{
"status": "ok",
"service": "Dewey MCP Server",
"version": "0.0.1-SNAPSHOT",
"protocol": "JSON-RPC 2.0",
"endpoint": "/mcp"
}
```
---
### OPTIONS /mcp
CORS preflight 요청을 처리하는 엔드포인트입니다.
**요청:**
```
OPTIONS http://localhost:8080/mcp
```
**응답:**
```
HTTP 200 OK
```
---
### POST /mcp
모든 MCP 요청은 이 단일 엔드포인트로 처리됩니다. 요청 본문은 JSON-RPC 2.0 형식을 따릅니다.
@ -65,10 +103,15 @@ MCP 서버를 초기화하고 서버 정보 및 지원 기능을 반환합니다
"id": "1",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": true,
"prompts": true,
"resources": true,
"logging": false
},
"clientInfo": {
"name": "client-name",
"name": "cursor-vscode",
"version": "1.0.0"
}
}
@ -82,11 +125,20 @@ MCP 서버를 초기화하고 서버 정보 및 지원 기능을 반환합니다
"jsonrpc": "2.0",
"id": "1",
"result": {
"protocolVersion": "2024-11-05",
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": {},
"resources": {},
"prompts": {}
"tools": {
"listChanged": true,
"supported": true
},
"resources": {
"listChanged": false,
"supported": true
},
"prompts": {
"listChanged": false,
"supported": false
}
},
"serverInfo": {
"name": "Dewey Memory Server",
@ -96,11 +148,43 @@ MCP 서버를 초기화하고 서버 정보 및 지원 기능을 반환합니다
}
```
**참고:**
- `protocolVersion`: 클라이언트가 보낸 버전을 그대로 반환합니다 (호환성)
- `capabilities.tools.listChanged`: `true`로 설정하여 Cursor가 `tools/list`를 호출하도록 유도합니다
- `capabilities.tools.supported`: `true`로 설정 (도구 기능 지원)
- `capabilities.resources.listChanged`: `false`로 설정 (리소스 목록 변경 없음)
- `capabilities.resources.supported`: `true`로 설정 (리소스 기능 지원)
- `capabilities.prompts.listChanged`: `false`로 설정 (프롬프트 목록 변경 없음)
- `capabilities.prompts.supported`: `false`로 설정 (프롬프트 기능 미지원)
**응답 DTO**: `McpInitializeResponse`
**참고:** `initialize` 요청 후 클라이언트는 `initialized` 알림을 보낼 수 있습니다. 이 알림은 응답이 필요 없습니다 (notification).
---
### 2. tools/list
### 2. initialized (알림)
`initialize` 요청 후 클라이언트가 보내는 알림입니다. 응답이 필요 없습니다.
#### 요청
```json
{
"jsonrpc": "2.0",
"method": "initialized"
}
```
**참고:** `id` 필드가 없으면 알림(notification)으로 처리됩니다.
#### 응답
응답이 필요 없습니다. HTTP 200 OK만 반환됩니다.
---
### 3. tools/list
사용 가능한 모든 도구 목록을 반환합니다.
@ -195,9 +279,11 @@ MCP 서버를 초기화하고 서버 정보 및 지원 기능을 반환합니다
**응답 DTO**: `McpToolResponse.ToolListResponse`
**참고:** `initialize` 응답에서 `capabilities.tools.listChanged: true`로 설정하면 클라이언트가 이 메서드를 호출합니다.
---
### 3. tools/call
### 4. tools/call
특정 도구를 실행합니다. 도구 실행 결과에 따라 응답 내용이 달라집니다.
@ -389,7 +475,7 @@ MCP 서버를 초기화하고 서버 정보 및 지원 기능을 반환합니다
---
### 4. resources/list
### 5. resources/list
사용 가능한 모든 리소스 목록을 반환합니다.
@ -432,7 +518,7 @@ MCP 서버를 초기화하고 서버 정보 및 지원 기능을 반환합니다
---
### 5. resources/read
### 6. resources/read
특정 리소스의 내용을 읽습니다.
@ -486,7 +572,7 @@ MCP 서버를 초기화하고 서버 정보 및 지원 기능을 반환합니다
---
### 6. prompts/list
### 7. prompts/list
사용 가능한 모든 프롬프트 목록을 반환합니다.
@ -539,7 +625,7 @@ MCP 서버를 초기화하고 서버 정보 및 지원 기능을 반환합니다
---
### 7. prompts/get
### 8. prompts/get
특정 프롬프트를 가져옵니다.
@ -602,6 +688,11 @@ MCP 서버를 초기화하고 서버 정보 및 지원 기능을 반환합니다
}
```
**참고:**
- 정상 응답일 때는 `error` 필드가 포함되지 않습니다 (JSON-RPC 2.0 표준)
- 에러 응답일 때는 `result` 필드가 포함되지 않습니다
- `@JsonInclude(JsonInclude.Include.NON_NULL)` 어노테이션으로 null 값 필드는 자동 제외됩니다
### 에러 코드
| 코드 | 의미 | 설명 |
@ -614,12 +705,22 @@ MCP 서버를 초기화하고 서버 정보 및 지원 기능을 반환합니다
**에러 처리**: `GlobalExceptionHandler`에서 전역 예외 처리
**추가 예외 처리:**
- `HttpRequestMethodNotSupportedException`: 지원되지 않는 HTTP 메서드 (405 Method Not Allowed)
- `HttpMessageNotReadableException`: JSON 파싱 오류 (400 Bad Request, JSON-RPC 2.0 에러 형식으로 반환)
---
## 사용 예시
### cURL 예시
#### 헬스 체크
```bash
curl -X GET http://localhost:8080/mcp
```
#### initialize
```bash
@ -630,8 +731,12 @@ curl -X POST http://localhost:8080/mcp \
"id": "1",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": true,
"prompts": true,
"resources": true
},
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
@ -795,6 +900,8 @@ public class MemoryResponse {
### 4. CORS 설정
- CORS는 모든 origin에서 허용되도록 설정되어 있습니다 (`/mcp/**`).
- 허용된 HTTP 메서드: `GET`, `POST`, `OPTIONS`
- 허용된 헤더: 모든 헤더 (`*`)
- 프로덕션 환경에서는 특정 origin만 허용하도록 변경하는 것을 권장합니다.
### 5. 예외 처리
@ -802,24 +909,50 @@ public class MemoryResponse {
- 전역 예외 처리는 `GlobalExceptionHandler`에서 수행됩니다.
- 유효성 검증 오류는 `MethodArgumentNotValidException`으로 처리됩니다.
- 모든 예외는 JSON-RPC 2.0 에러 형식으로 변환됩니다.
- `HttpRequestMethodNotSupportedException`: 405 Method Not Allowed 응답 반환
- `HttpMessageNotReadableException`: JSON 파싱 오류를 JSON-RPC 2.0 에러 형식으로 변환하여 반환
### 6. 로깅
- 모든 요청과 응답이 상세하게 로깅됩니다.
- 요청 본문은 JSON 포맷으로 로깅됩니다 (`=== MCP 요청 수신 ===`).
- 응답 본문은 JSON 포맷으로 로깅됩니다 (`=== MCP 응답 전송 ===`).
- 에러 응답도 별도로 로깅됩니다 (`=== MCP 에러 응답 전송 ===`).
- 헬스 체크와 OPTIONS 요청도 로깅됩니다.
### 6. 구현 상태
- ✅ 기본 MCP 프로토콜 구현 완료
- ✅ 도메인 모델 및 Repository 생성 완료
- ✅ MVC 구조 재설계 완료
- ✅ 헬스 체크 엔드포인트 추가 (GET /mcp)
- ✅ CORS preflight 처리 (OPTIONS /mcp)
- ✅ initialized 알림 처리
- ✅ 예외 처리 개선 (HTTP 메서드, JSON 파싱)
- ✅ `capabilities``supported` 필드 추가
- ✅ JSON-RPC 2.0 표준 준수 (정상 응답에 error 필드 제외)
- ✅ 요청/응답 전체 로깅 기능 추가
- ⏳ Redis 연동 (TODO)
- ⏳ 벡터 검색 구현 (TODO)
- ⏳ 배치 처리 구현 (TODO)
### 7. Cursor IDE 연동
- ✅ Cursor IDE에서 HTTP 기반 MCP 서버로 연결 성공
- ✅ `mcp.json` 설정 파일에서 `url` 필드로 서버 주소 지정
- ✅ `tools/list` 요청 정상 처리 확인
- ✅ `capabilities.supported` 필드로 기능 지원 여부 명시
---
## 버전 정보
- **문서 버전**: 2.0.0
- **문서 버전**: 2.2.0
- **최종 업데이트**: 2025-12-10
- **서버 버전**: 0.0.1-SNAPSHOT
- **아키텍처**: MVC (Model-View-Controller)
- **프로토콜 버전**: 2025-06-18 (클라이언트 호환)
- **상태**: ✅ Cursor IDE 연동 성공
---

View File

@ -1,5 +1,7 @@
package com.pandol365.dewey.api.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pandol365.dewey.api.dto.request.McpJsonRpcRequest;
import com.pandol365.dewey.api.dto.response.McpJsonRpcResponse;
import com.pandol365.dewey.api.service.McpService;
@ -23,6 +25,7 @@ import java.util.Map;
public class McpController {
private final McpService mcpService;
private final ObjectMapper objectMapper;
/**
* 헬스 체크 엔드포인트 (GET 요청 처리)
@ -30,13 +33,22 @@ public class McpController {
*/
@GetMapping(produces = {"application/json", "text/plain", "*/*"})
public ResponseEntity<Map<String, Object>> healthCheck() {
log.info("헬스 체크 요청 수신");
log.info("=== 헬스 체크 요청 수신 ===");
Map<String, Object> 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");
try {
String responseJson = objectMapper.writeValueAsString(response);
log.info("=== 헬스 체크 응답 전송 ===\n{}", responseJson);
} catch (JsonProcessingException e) {
log.warn("응답 JSON 변환 실패: {}", e.getMessage());
}
return ResponseEntity.ok()
.header("Content-Type", "application/json")
.body(response);
@ -47,7 +59,8 @@ public class McpController {
*/
@RequestMapping(method = RequestMethod.OPTIONS)
public ResponseEntity<Void> options() {
log.info("OPTIONS 요청 수신 (CORS preflight)");
log.info("=== OPTIONS 요청 수신 (CORS preflight) ===");
log.info("=== OPTIONS 응답 전송 (HTTP 200 OK) ===");
return ResponseEntity.ok().build();
}
@ -57,21 +70,45 @@ public class McpController {
*/
@PostMapping
public ResponseEntity<McpJsonRpcResponse> handleMcpRequest(@RequestBody McpJsonRpcRequest request) {
log.info("MCP 요청 수신: method={}, id={}", request.getMethod(), request.getId());
// 요청 전체 로깅
try {
String requestJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(request);
log.info("=== MCP 요청 수신 ===\n{}", requestJson);
} catch (JsonProcessingException e) {
log.warn("요청 JSON 변환 실패: {}", e.getMessage());
log.info("=== MCP 요청 수신 ===\nmethod={}, id={}, request={}",
request.getMethod(), request.getId(), request);
}
try {
Object result = processRequest(request);
// initialized 알림은 응답이 필요 없음 (notification)
if ("initialized".equals(request.getMethod()) && request.getId() == null) {
log.info("=== initialized 알림 처리 완료 (응답 불필요) ===");
return ResponseEntity.ok().build();
}
McpJsonRpcResponse response = McpJsonRpcResponse.builder()
.jsonrpc("2.0")
.id(request.getId())
.result(result)
.build();
// 응답 전체 로깅
try {
String responseJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(response);
log.info("=== MCP 응답 전송 ===\n{}", responseJson);
} catch (JsonProcessingException e) {
log.warn("응답 JSON 변환 실패: {}", e.getMessage());
log.info("=== MCP 응답 전송 ===\nmethod={}, id={}, result={}",
request.getMethod(), request.getId(), result);
}
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("MCP 요청 처리 중 오류 발생", e);
log.error("=== MCP 요청 처리 중 오류 발생 ===", e);
McpJsonRpcResponse.McpError error = McpJsonRpcResponse.McpError.builder()
.code(-32603) // Internal error
@ -85,6 +122,16 @@ public class McpController {
.error(error)
.build();
// 에러 응답 로깅
try {
String responseJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(response);
log.error("=== MCP 에러 응답 전송 ===\n{}", responseJson);
} catch (JsonProcessingException ex) {
log.warn("에러 응답 JSON 변환 실패: {}", ex.getMessage());
log.error("=== MCP 에러 응답 전송 ===\nmethod={}, id={}, error={}",
request.getMethod(), request.getId(), error);
}
return ResponseEntity.ok(response);
}
}
@ -99,6 +146,11 @@ public class McpController {
return switch (method) {
case "initialize" -> mcpService.initialize(params);
case "initialized" -> {
// MCP 프로토콜: initialize 클라이언트가 보내는 알림
log.info("MCP initialized 알림 수신");
yield Map.of("status", "ok");
}
case "tools/list" -> mcpService.listTools();
case "tools/call" -> {
Object args = params != null ? params.getArguments() : null;

View File

@ -1,5 +1,6 @@
package com.pandol365.dewey.api.dto.response;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
@ -13,13 +14,14 @@ import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class McpJsonRpcResponse {
@JsonProperty("jsonrpc")
@Builder.Default
private String jsonrpc = "2.0";
private String id;
private Object id;
private Object result;

View File

@ -29,18 +29,41 @@ public class McpServiceImpl implements McpService {
public McpInitializeResponse initialize(McpJsonRpcRequest.McpParams params) {
log.info("MCP 서버 초기화 요청: {}", params);
// Cursor가 보낸 프로토콜 버전 사용 (호환성)
String protocolVersion = params != null && params.getProtocolVersion() != null
? params.getProtocolVersion()
: PROTOCOL_VERSION;
// MCP 표준 capabilities 형식 - Cursor가 tools/list를 호출하도록 설정
Map<String, Object> capabilities = new HashMap<>();
capabilities.put("tools", Map.of());
capabilities.put("resources", Map.of());
capabilities.put("prompts", Map.of());
// tools capability: listChanged를 true로 설정하여 Cursor가 tools/list를 호출하도록 유도
Map<String, Object> toolsCapability = new HashMap<>();
toolsCapability.put("listChanged", true);
toolsCapability.put("supported", true); // tools 지원
capabilities.put("tools", toolsCapability);
// resources capability
Map<String, Object> resourcesCapability = new HashMap<>();
resourcesCapability.put("listChanged", false);
resourcesCapability.put("supported", true); // resources 지원
capabilities.put("resources", resourcesCapability);
// prompts capability
Map<String, Object> promptsCapability = new HashMap<>();
promptsCapability.put("listChanged", false);
promptsCapability.put("supported", false); // prompts 미지원
capabilities.put("prompts", promptsCapability);
McpInitializeResponse.ServerInfo serverInfo = McpInitializeResponse.ServerInfo.builder()
.name(SERVER_NAME)
.version(SERVER_VERSION)
.build();
log.info("MCP 초기화 응답: protocolVersion={}, capabilities={}", protocolVersion, capabilities);
return McpInitializeResponse.builder()
.protocolVersion(PROTOCOL_VERSION)
.protocolVersion(protocolVersion)
.capabilities(capabilities)
.serverInfo(serverInfo)
.build();