Cursor MCP 연결 성공
This commit is contained in:
parent
29f381ae81
commit
de5eede11d
167
API명세서.md
167
API명세서.md
|
|
@ -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 연동 성공
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue