MVC구조로 변경
This commit is contained in:
parent
5947340574
commit
dc8015a3bf
|
|
@ -0,0 +1,863 @@
|
||||||
|
# Dewey MCP 서버 API 명세서
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
Dewey MCP (Model Context Protocol) 서버는 JSON-RPC 2.0 프로토콜을 기반으로 AI 장기기억 시스템의 기능을 제공합니다.
|
||||||
|
|
||||||
|
- **프로토콜 버전**: 2024-11-05
|
||||||
|
- **서버 이름**: Dewey Memory Server
|
||||||
|
- **서버 버전**: 0.0.1-SNAPSHOT
|
||||||
|
- **Base URL**: `http://localhost:8080/mcp`
|
||||||
|
- **아키텍처**: MVC (Model-View-Controller) 패턴
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 아키텍처
|
||||||
|
|
||||||
|
### MVC 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
Controller (McpController)
|
||||||
|
↓
|
||||||
|
API Service (McpService)
|
||||||
|
↓
|
||||||
|
Domain Service (MemoryService)
|
||||||
|
↓
|
||||||
|
Repository (MemoryRepository)
|
||||||
|
↓
|
||||||
|
Database (PostgreSQL / Redis)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 계층별 역할
|
||||||
|
|
||||||
|
- **Controller**: HTTP 요청 처리 및 라우팅
|
||||||
|
- **API Service**: Controller와 Domain Service 사이의 어댑터
|
||||||
|
- **Domain Service**: 비즈니스 로직 처리
|
||||||
|
- **Repository**: 데이터 접근 계층
|
||||||
|
- **Model**: 도메인 엔티티 및 DTO
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 엔드포인트
|
||||||
|
|
||||||
|
### POST /mcp
|
||||||
|
|
||||||
|
모든 MCP 요청은 이 단일 엔드포인트로 처리됩니다. 요청 본문은 JSON-RPC 2.0 형식을 따릅니다.
|
||||||
|
|
||||||
|
**요청 헤더:**
|
||||||
|
```
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP 메서드
|
||||||
|
|
||||||
|
### 1. initialize
|
||||||
|
|
||||||
|
MCP 서버를 초기화하고 서버 정보 및 지원 기능을 반환합니다.
|
||||||
|
|
||||||
|
#### 요청
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "1",
|
||||||
|
"method": "initialize",
|
||||||
|
"params": {
|
||||||
|
"protocolVersion": "2024-11-05",
|
||||||
|
"capabilities": {},
|
||||||
|
"clientInfo": {
|
||||||
|
"name": "client-name",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 응답
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "1",
|
||||||
|
"result": {
|
||||||
|
"protocolVersion": "2024-11-05",
|
||||||
|
"capabilities": {
|
||||||
|
"tools": {},
|
||||||
|
"resources": {},
|
||||||
|
"prompts": {}
|
||||||
|
},
|
||||||
|
"serverInfo": {
|
||||||
|
"name": "Dewey Memory Server",
|
||||||
|
"version": "0.0.1-SNAPSHOT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**응답 DTO**: `McpInitializeResponse`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. tools/list
|
||||||
|
|
||||||
|
사용 가능한 모든 도구 목록을 반환합니다.
|
||||||
|
|
||||||
|
#### 요청
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "2",
|
||||||
|
"method": "tools/list"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 응답
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "2",
|
||||||
|
"result": {
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"name": "store_memory",
|
||||||
|
"description": "메모리를 Redis에 임시 저장합니다",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"memory_text": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "저장할 메모리 텍스트"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "사용자 ID"
|
||||||
|
},
|
||||||
|
"importance": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "중요도 (1-5)",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["memory_text", "user_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "retrieve_memory",
|
||||||
|
"description": "사용자의 메모리를 조회합니다",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"user_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "사용자 ID"
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "조회할 메모리 개수",
|
||||||
|
"default": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["user_id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "search_memory",
|
||||||
|
"description": "벡터 유사도 기반으로 메모리를 검색합니다",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "검색 쿼리"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "사용자 ID (선택)"
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "검색 결과 개수",
|
||||||
|
"default": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["query"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**응답 DTO**: `McpToolResponse.ToolListResponse`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. tools/call
|
||||||
|
|
||||||
|
특정 도구를 실행합니다. 도구 실행 결과에 따라 응답 내용이 달라집니다.
|
||||||
|
|
||||||
|
#### 요청 형식
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"arguments": {
|
||||||
|
"name": "도구명",
|
||||||
|
"arguments": {
|
||||||
|
"도구별_파라미터": "값"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 응답 형식
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": {
|
||||||
|
"isError": false,
|
||||||
|
"content": "실행 결과 메시지",
|
||||||
|
"parts": [],
|
||||||
|
"metadata": {
|
||||||
|
"tool": "도구명",
|
||||||
|
"timestamp": 1701234567890
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**응답 DTO**: `McpToolCallResponse`
|
||||||
|
|
||||||
|
#### 도구별 사용 예시
|
||||||
|
|
||||||
|
##### store_memory
|
||||||
|
|
||||||
|
메모리를 Redis에 임시 저장합니다.
|
||||||
|
|
||||||
|
**요청:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"arguments": {
|
||||||
|
"name": "store_memory",
|
||||||
|
"arguments": {
|
||||||
|
"memory_text": "오늘 사용자가 피자를 주문했습니다",
|
||||||
|
"user_id": "user123",
|
||||||
|
"importance": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**성공 응답:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"result": {
|
||||||
|
"isError": false,
|
||||||
|
"content": "Memory stored successfully",
|
||||||
|
"parts": [],
|
||||||
|
"metadata": {
|
||||||
|
"tool": "store_memory",
|
||||||
|
"timestamp": 1701234567890
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**비즈니스 로직**: `MemoryService.storeTemporaryMemory()` 호출
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
##### retrieve_memory
|
||||||
|
|
||||||
|
사용자의 영구 메모리를 PostgreSQL에서 조회합니다.
|
||||||
|
|
||||||
|
**요청:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "5",
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"arguments": {
|
||||||
|
"name": "retrieve_memory",
|
||||||
|
"arguments": {
|
||||||
|
"user_id": "user123",
|
||||||
|
"limit": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**성공 응답:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "5",
|
||||||
|
"result": {
|
||||||
|
"isError": false,
|
||||||
|
"content": "Retrieved 10 memories",
|
||||||
|
"parts": [],
|
||||||
|
"metadata": {
|
||||||
|
"tool": "retrieve_memory",
|
||||||
|
"timestamp": 1701234567890
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**비즈니스 로직**: `MemoryService.getPermanentMemories()` 호출
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
##### search_memory
|
||||||
|
|
||||||
|
벡터 유사도 기반으로 메모리를 검색합니다.
|
||||||
|
|
||||||
|
**요청:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "6",
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"arguments": {
|
||||||
|
"name": "search_memory",
|
||||||
|
"arguments": {
|
||||||
|
"query": "커피",
|
||||||
|
"user_id": "user123",
|
||||||
|
"limit": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**성공 응답:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "6",
|
||||||
|
"result": {
|
||||||
|
"isError": false,
|
||||||
|
"content": "Found 3 matching memories",
|
||||||
|
"parts": [],
|
||||||
|
"metadata": {
|
||||||
|
"tool": "search_memory",
|
||||||
|
"timestamp": 1701234567890
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**비즈니스 로직**: `MemoryService.searchMemoriesByVector()` 호출
|
||||||
|
|
||||||
|
**에러 응답 예시:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "6",
|
||||||
|
"result": {
|
||||||
|
"isError": true,
|
||||||
|
"content": "Error: Unknown tool: invalid_tool",
|
||||||
|
"parts": [],
|
||||||
|
"metadata": {
|
||||||
|
"tool": "invalid_tool",
|
||||||
|
"timestamp": 1701234567890
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. resources/list
|
||||||
|
|
||||||
|
사용 가능한 모든 리소스 목록을 반환합니다.
|
||||||
|
|
||||||
|
#### 요청
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "7",
|
||||||
|
"method": "resources/list"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 응답
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "7",
|
||||||
|
"result": {
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"uri": "memory://recent",
|
||||||
|
"name": "Recent Memories",
|
||||||
|
"description": "최근 저장된 메모리 리소스",
|
||||||
|
"mimeType": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "memory://long-term",
|
||||||
|
"name": "Long-term Memories",
|
||||||
|
"description": "장기 저장된 메모리 리소스",
|
||||||
|
"mimeType": "application/json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**응답 DTO**: `McpResourceResponse.ResourceListResponse`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. resources/read
|
||||||
|
|
||||||
|
특정 리소스의 내용을 읽습니다.
|
||||||
|
|
||||||
|
#### 요청
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "8",
|
||||||
|
"method": "resources/read",
|
||||||
|
"params": {
|
||||||
|
"uri": "memory://recent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
또는
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "8",
|
||||||
|
"method": "resources/read",
|
||||||
|
"params": {
|
||||||
|
"arguments": {
|
||||||
|
"uri": "memory://recent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 응답
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "8",
|
||||||
|
"result": {
|
||||||
|
"uri": "memory://recent",
|
||||||
|
"mimeType": "application/json",
|
||||||
|
"text": "{\"message\": \"Resource content for memory://recent\"}",
|
||||||
|
"metadata": {
|
||||||
|
"uri": "memory://recent",
|
||||||
|
"timestamp": 1701234567890
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**응답 DTO**: `McpResourceResponse.ResourceReadResponse`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. prompts/list
|
||||||
|
|
||||||
|
사용 가능한 모든 프롬프트 목록을 반환합니다.
|
||||||
|
|
||||||
|
#### 요청
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "9",
|
||||||
|
"method": "prompts/list"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 응답
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "9",
|
||||||
|
"result": {
|
||||||
|
"prompts": [
|
||||||
|
{
|
||||||
|
"name": "memory_summary",
|
||||||
|
"description": "메모리 목록을 요약하는 프롬프트",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "memories",
|
||||||
|
"description": "요약할 메모리 텍스트 목록",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "memory_search",
|
||||||
|
"description": "메모리 검색을 위한 프롬프트",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "query",
|
||||||
|
"description": "검색할 키워드",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**응답 DTO**: `McpPromptResponse.PromptListResponse`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. prompts/get
|
||||||
|
|
||||||
|
특정 프롬프트를 가져옵니다.
|
||||||
|
|
||||||
|
#### 요청
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "10",
|
||||||
|
"method": "prompts/get",
|
||||||
|
"params": {
|
||||||
|
"arguments": {
|
||||||
|
"name": "memory_summary",
|
||||||
|
"arguments": {
|
||||||
|
"memories": ["메모리1", "메모리2", "메모리3"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 응답
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "10",
|
||||||
|
"result": {
|
||||||
|
"name": "memory_summary",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Prompt: memory_summary"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"arguments": {
|
||||||
|
"memories": ["메모리1", "메모리2", "메모리3"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**응답 DTO**: `McpPromptResponse.PromptGetResponse`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 에러 응답
|
||||||
|
|
||||||
|
에러가 발생한 경우 다음과 같은 형식으로 응답됩니다:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "1",
|
||||||
|
"error": {
|
||||||
|
"code": -32603,
|
||||||
|
"message": "Internal error",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 에러 코드
|
||||||
|
|
||||||
|
| 코드 | 의미 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| `-32600` | Invalid Request | 요청 형식이 잘못됨 |
|
||||||
|
| `-32601` | Method not found | 존재하지 않는 메서드 호출 |
|
||||||
|
| `-32602` | Invalid params | 파라미터가 잘못됨 |
|
||||||
|
| `-32603` | Internal error | 서버 내부 오류 |
|
||||||
|
| `-32700` | Parse error | JSON 파싱 오류 |
|
||||||
|
|
||||||
|
**에러 처리**: `GlobalExceptionHandler`에서 전역 예외 처리
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 사용 예시
|
||||||
|
|
||||||
|
### cURL 예시
|
||||||
|
|
||||||
|
#### initialize
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/mcp \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "1",
|
||||||
|
"method": "initialize",
|
||||||
|
"params": {
|
||||||
|
"protocolVersion": "2024-11-05",
|
||||||
|
"capabilities": {},
|
||||||
|
"clientInfo": {
|
||||||
|
"name": "test-client",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### tools/list
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/mcp \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "2",
|
||||||
|
"method": "tools/list"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### tools/call - store_memory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/mcp \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"arguments": {
|
||||||
|
"name": "store_memory",
|
||||||
|
"arguments": {
|
||||||
|
"memory_text": "사용자는 파이썬을 좋아합니다",
|
||||||
|
"user_id": "user123",
|
||||||
|
"importance": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### tools/call - retrieve_memory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/mcp \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"arguments": {
|
||||||
|
"name": "retrieve_memory",
|
||||||
|
"arguments": {
|
||||||
|
"user_id": "user123",
|
||||||
|
"limit": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### tools/call - search_memory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/mcp \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "5",
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"arguments": {
|
||||||
|
"name": "search_memory",
|
||||||
|
"arguments": {
|
||||||
|
"query": "커피",
|
||||||
|
"user_id": "user123",
|
||||||
|
"limit": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 데이터 모델
|
||||||
|
|
||||||
|
### Memory (도메인 엔티티)
|
||||||
|
|
||||||
|
PostgreSQL에 저장되는 영구 메모리 엔티티입니다.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Entity
|
||||||
|
@Table(name = "memories")
|
||||||
|
public class Memory {
|
||||||
|
private Long id;
|
||||||
|
private String userId;
|
||||||
|
private String memoryText;
|
||||||
|
private Integer importance; // 1~5
|
||||||
|
private String tags; // JSON 형식
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TemporaryMemory (도메인 모델)
|
||||||
|
|
||||||
|
Redis에 저장되는 임시 메모리 모델입니다.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class TemporaryMemory {
|
||||||
|
private String id; // Redis key
|
||||||
|
private String userId;
|
||||||
|
private String memoryText;
|
||||||
|
private Integer importance;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime expiresAt; // TTL 기반
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MemoryResponse (응답 DTO)
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class MemoryResponse {
|
||||||
|
private Long id;
|
||||||
|
private String userId;
|
||||||
|
private String memoryText;
|
||||||
|
private Integer importance;
|
||||||
|
private String tags;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 참고사항
|
||||||
|
|
||||||
|
### 1. 요청/응답 형식
|
||||||
|
|
||||||
|
- 모든 요청은 `Content-Type: application/json` 헤더가 필요합니다.
|
||||||
|
- `id` 필드는 요청과 응답을 매칭하기 위해 사용됩니다.
|
||||||
|
- 모든 응답은 `jsonrpc: "2.0"` 필드를 포함합니다.
|
||||||
|
|
||||||
|
### 2. 아키텍처
|
||||||
|
|
||||||
|
- **MVC 패턴**을 따르며, 각 계층이 명확히 분리되어 있습니다.
|
||||||
|
- **Controller**는 요청 처리만 담당합니다.
|
||||||
|
- **API Service**는 Controller와 Domain Service 사이의 어댑터 역할을 합니다.
|
||||||
|
- **Domain Service**는 비즈니스 로직을 처리합니다.
|
||||||
|
- **Repository**는 데이터 접근만 담당합니다.
|
||||||
|
|
||||||
|
### 3. 데이터 저장
|
||||||
|
|
||||||
|
- **임시 메모리**: Redis에 저장 (TTL 3일)
|
||||||
|
- **영구 메모리**: PostgreSQL에 저장
|
||||||
|
- **벡터 검색**: PostgreSQL의 pgvector 확장 사용 (향후 구현)
|
||||||
|
|
||||||
|
### 4. CORS 설정
|
||||||
|
|
||||||
|
- CORS는 모든 origin에서 허용되도록 설정되어 있습니다 (`/mcp/**`).
|
||||||
|
- 프로덕션 환경에서는 특정 origin만 허용하도록 변경하는 것을 권장합니다.
|
||||||
|
|
||||||
|
### 5. 예외 처리
|
||||||
|
|
||||||
|
- 전역 예외 처리는 `GlobalExceptionHandler`에서 수행됩니다.
|
||||||
|
- 유효성 검증 오류는 `MethodArgumentNotValidException`으로 처리됩니다.
|
||||||
|
- 모든 예외는 JSON-RPC 2.0 에러 형식으로 변환됩니다.
|
||||||
|
|
||||||
|
### 6. 구현 상태
|
||||||
|
|
||||||
|
- ✅ 기본 MCP 프로토콜 구현 완료
|
||||||
|
- ✅ 도메인 모델 및 Repository 생성 완료
|
||||||
|
- ✅ MVC 구조 재설계 완료
|
||||||
|
- ⏳ Redis 연동 (TODO)
|
||||||
|
- ⏳ 벡터 검색 구현 (TODO)
|
||||||
|
- ⏳ 배치 처리 구현 (TODO)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 버전 정보
|
||||||
|
|
||||||
|
- **문서 버전**: 2.0.0
|
||||||
|
- **최종 업데이트**: 2025-12-10
|
||||||
|
- **서버 버전**: 0.0.1-SNAPSHOT
|
||||||
|
- **아키텍처**: MVC (Model-View-Controller)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 관련 파일 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
src/main/java/com/pandol365/dewey/
|
||||||
|
├── api/
|
||||||
|
│ ├── controller/
|
||||||
|
│ │ └── McpController.java # Controller 계층
|
||||||
|
│ ├── dto/
|
||||||
|
│ │ ├── request/ # 요청 DTO
|
||||||
|
│ │ │ ├── McpJsonRpcRequest.java
|
||||||
|
│ │ │ ├── MemoryStoreRequest.java
|
||||||
|
│ │ │ └── MemorySearchRequest.java
|
||||||
|
│ │ └── response/ # 응답 DTO (View)
|
||||||
|
│ │ ├── McpJsonRpcResponse.java
|
||||||
|
│ │ ├── McpInitializeResponse.java
|
||||||
|
│ │ ├── McpToolResponse.java
|
||||||
|
│ │ ├── McpResourceResponse.java
|
||||||
|
│ │ ├── McpPromptResponse.java
|
||||||
|
│ │ ├── McpToolCallResponse.java
|
||||||
|
│ │ └── MemoryResponse.java
|
||||||
|
│ └── service/ # API Service 계층
|
||||||
|
│ ├── McpService.java
|
||||||
|
│ └── impl/
|
||||||
|
│ └── McpServiceImpl.java
|
||||||
|
├── domain/
|
||||||
|
│ └── memory/
|
||||||
|
│ ├── model/ # 도메인 모델 (Model)
|
||||||
|
│ │ ├── Memory.java
|
||||||
|
│ │ └── TemporaryMemory.java
|
||||||
|
│ ├── repository/ # Repository 계층
|
||||||
|
│ │ └── MemoryRepository.java
|
||||||
|
│ └── service/ # Domain Service 계층
|
||||||
|
│ ├── MemoryService.java
|
||||||
|
│ └── impl/
|
||||||
|
│ └── MemoryServiceImpl.java
|
||||||
|
└── exception/
|
||||||
|
└── GlobalExceptionHandler.java # 예외 처리
|
||||||
|
```
|
||||||
|
|
@ -2,8 +2,11 @@ package com.pandol365.dewey;
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.ai.vectorstore.pgvector.autoconfigure.PgVectorStoreAutoConfiguration;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication(exclude = {
|
||||||
|
PgVectorStoreAutoConfiguration.class
|
||||||
|
})
|
||||||
public class DeweyApplication {
|
public class DeweyApplication {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(DeweyApplication.class, args);
|
SpringApplication.run(DeweyApplication.class, args);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package com.pandol365.dewey.api.dto.request;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP JSON-RPC 2.0 요청 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class McpJsonRpcRequest {
|
||||||
|
|
||||||
|
@JsonProperty("jsonrpc")
|
||||||
|
@Builder.Default
|
||||||
|
private String jsonrpc = "2.0";
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private String method;
|
||||||
|
|
||||||
|
private McpParams params;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class McpParams {
|
||||||
|
private String protocolVersion;
|
||||||
|
private Map<String, Object> capabilities;
|
||||||
|
private Map<String, Object> clientInfo;
|
||||||
|
private String name;
|
||||||
|
private String uri;
|
||||||
|
private String prompt;
|
||||||
|
private Object arguments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.pandol365.dewey.api.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메모리 검색 요청 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MemorySearchRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "검색 쿼리는 필수입니다")
|
||||||
|
private String query;
|
||||||
|
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
@Min(value = 1, message = "limit은 1 이상이어야 합니다")
|
||||||
|
@Builder.Default
|
||||||
|
private Integer limit = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.pandol365.dewey.api.dto.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메모리 저장 요청 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MemoryStoreRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "메모리 텍스트는 필수입니다")
|
||||||
|
private String memoryText;
|
||||||
|
|
||||||
|
@NotBlank(message = "사용자 ID는 필수입니다")
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
@Min(value = 1, message = "중요도는 1 이상이어야 합니다")
|
||||||
|
@Max(value = 5, message = "중요도는 5 이하여야 합니다")
|
||||||
|
private Integer importance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.pandol365.dewey.api.dto.response;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP initialize 메서드 응답 DTO (View)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class McpInitializeResponse {
|
||||||
|
|
||||||
|
private String protocolVersion;
|
||||||
|
|
||||||
|
private Map<String, Object> capabilities;
|
||||||
|
|
||||||
|
private ServerInfo serverInfo;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class ServerInfo {
|
||||||
|
private String name;
|
||||||
|
private String version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.pandol365.dewey.api.dto.response;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP JSON-RPC 2.0 응답 DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class McpJsonRpcResponse {
|
||||||
|
|
||||||
|
@JsonProperty("jsonrpc")
|
||||||
|
@Builder.Default
|
||||||
|
private String jsonrpc = "2.0";
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private Object result;
|
||||||
|
|
||||||
|
private McpError error;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class McpError {
|
||||||
|
private Integer code;
|
||||||
|
private String message;
|
||||||
|
private Object data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package com.pandol365.dewey.api.dto.response;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP Prompt 응답 DTO (View)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class McpPromptResponse {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
private List<PromptArgument> arguments;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class PromptArgument {
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private Boolean required;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class PromptListResponse {
|
||||||
|
private List<McpPromptResponse> prompts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class PromptGetResponse {
|
||||||
|
private String name;
|
||||||
|
private List<PromptMessage> messages;
|
||||||
|
private Map<String, Object> arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class PromptMessage {
|
||||||
|
private String role;
|
||||||
|
private Object content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
package com.pandol365.dewey.api.dto.response;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP Resource 응답 DTO (View)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class McpResourceResponse {
|
||||||
|
|
||||||
|
private String uri;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
private String mimeType;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class ResourceListResponse {
|
||||||
|
private List<McpResourceResponse> resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class ResourceReadResponse {
|
||||||
|
private String uri;
|
||||||
|
private String mimeType;
|
||||||
|
private String text;
|
||||||
|
private Map<String, Object> metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.pandol365.dewey.api.dto.response;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP tools/call 메서드 응답 DTO (View)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class McpToolCallResponse {
|
||||||
|
|
||||||
|
@JsonProperty("isError")
|
||||||
|
private Boolean isError;
|
||||||
|
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
private List<Map<String, Object>> parts;
|
||||||
|
|
||||||
|
private Map<String, Object> metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.pandol365.dewey.api.dto.response;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP Tool 응답 DTO (View)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class McpToolResponse {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@JsonProperty("inputSchema")
|
||||||
|
private Map<String, Object> inputSchema;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class ToolListResponse {
|
||||||
|
private List<McpToolResponse> tools;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.pandol365.dewey.api.dto.response;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메모리 응답 DTO (View)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MemoryResponse {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
private String memoryText;
|
||||||
|
|
||||||
|
private Integer importance;
|
||||||
|
|
||||||
|
private String tags;
|
||||||
|
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.pandol365.dewey.api.request;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP JSON-RPC 2.0 Request DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class McpRequest {
|
||||||
|
|
||||||
|
@JsonProperty("jsonrpc")
|
||||||
|
@Builder.Default
|
||||||
|
private String jsonrpc = "2.0";
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private String method;
|
||||||
|
|
||||||
|
private McpParams params;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class McpParams {
|
||||||
|
private String name;
|
||||||
|
private String version;
|
||||||
|
private Object arguments;
|
||||||
|
private String uri;
|
||||||
|
private String prompt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
package com.pandol365.dewey.api.response;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class McpHealthResponse {
|
|
||||||
private String status;
|
|
||||||
private LocalDateTime timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
package com.pandol365.dewey.api.response;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class McpInfoResponse {
|
|
||||||
private String name;
|
|
||||||
private String version;
|
|
||||||
private String description;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.pandol365.dewey.api.response;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP initialize 메서드 응답 결과
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class McpInitializeResult {
|
||||||
|
|
||||||
|
private String protocolVersion;
|
||||||
|
|
||||||
|
private Map<String, Object> capabilities;
|
||||||
|
|
||||||
|
private ServerInfo serverInfo;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class ServerInfo {
|
||||||
|
private String name;
|
||||||
|
private String version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package com.pandol365.dewey.api.response;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP Prompt 정의
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class McpPrompt {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
private List<PromptArgument> arguments;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class PromptArgument {
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private Boolean required;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class PromptListResult {
|
||||||
|
private List<McpPrompt> prompts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class PromptGetResult {
|
||||||
|
private String name;
|
||||||
|
private List<PromptMessage> messages;
|
||||||
|
private Map<String, Object> arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class PromptMessage {
|
||||||
|
private String role;
|
||||||
|
private Object content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
package com.pandol365.dewey.api.response;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP Resource 정의
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class McpResource {
|
||||||
|
|
||||||
|
private String uri;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
private String mimeType;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class ResourceListResult {
|
||||||
|
private List<McpResource> resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class ResourceReadResult {
|
||||||
|
private String uri;
|
||||||
|
private String mimeType;
|
||||||
|
private String text;
|
||||||
|
private Map<String, Object> metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.pandol365.dewey.api.response;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP JSON-RPC 2.0 Response DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class McpResponse {
|
||||||
|
|
||||||
|
@JsonProperty("jsonrpc")
|
||||||
|
@Builder.Default
|
||||||
|
private String jsonrpc = "2.0";
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private Object result;
|
||||||
|
|
||||||
|
private McpError error;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class McpError {
|
||||||
|
private Integer code;
|
||||||
|
private String message;
|
||||||
|
private Object data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.pandol365.dewey.api.response;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP Tool 정의
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class McpTool {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@JsonProperty("inputSchema")
|
||||||
|
private Map<String, Object> inputSchema;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class ToolListResult {
|
||||||
|
private List<McpTool> tools;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.pandol365.dewey.api.response;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP tools/call 메서드 응답 결과
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class McpToolCallResult {
|
||||||
|
|
||||||
|
@JsonProperty("isError")
|
||||||
|
private Boolean isError;
|
||||||
|
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
private List<Map<String, Object>> parts;
|
||||||
|
|
||||||
|
private Map<String, Object> metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package com.pandol365.dewey.api.service;
|
||||||
|
|
||||||
|
import com.pandol365.dewey.api.dto.request.McpJsonRpcRequest;
|
||||||
|
import com.pandol365.dewey.api.dto.response.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 서버 비즈니스 로직 인터페이스
|
||||||
|
* Controller와 Domain Service 사이의 어댑터 역할
|
||||||
|
*/
|
||||||
|
public interface McpService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 서버 초기화
|
||||||
|
*/
|
||||||
|
McpInitializeResponse initialize(McpJsonRpcRequest.McpParams params);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용 가능한 도구 목록 반환
|
||||||
|
*/
|
||||||
|
McpToolResponse.ToolListResponse listTools();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 도구 호출 실행
|
||||||
|
*/
|
||||||
|
McpToolCallResponse callTool(String name, Object arguments);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용 가능한 리소스 목록 반환
|
||||||
|
*/
|
||||||
|
McpResourceResponse.ResourceListResponse listResources();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 리소스 읽기
|
||||||
|
*/
|
||||||
|
McpResourceResponse.ResourceReadResponse readResource(String uri);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용 가능한 프롬프트 목록 반환
|
||||||
|
*/
|
||||||
|
McpPromptResponse.PromptListResponse listPrompts();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 프롬프트 가져오기
|
||||||
|
*/
|
||||||
|
McpPromptResponse.PromptGetResponse getPrompt(String name, Object arguments);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
package com.pandol365.dewey.api.service.impl;
|
||||||
|
|
||||||
|
import com.pandol365.dewey.api.dto.request.McpJsonRpcRequest;
|
||||||
|
import com.pandol365.dewey.api.dto.response.*;
|
||||||
|
import com.pandol365.dewey.api.service.McpService;
|
||||||
|
import com.pandol365.dewey.domain.memory.service.MemoryService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 서버 구현체
|
||||||
|
* Controller와 Domain Service 사이의 어댑터 역할 수행
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class McpServiceImpl implements McpService {
|
||||||
|
|
||||||
|
private static final String PROTOCOL_VERSION = "2024-11-05";
|
||||||
|
private static final String SERVER_NAME = "Dewey Memory Server";
|
||||||
|
private static final String SERVER_VERSION = "0.0.1-SNAPSHOT";
|
||||||
|
|
||||||
|
private final MemoryService memoryService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public McpInitializeResponse initialize(McpJsonRpcRequest.McpParams params) {
|
||||||
|
log.info("MCP 서버 초기화 요청: {}", params);
|
||||||
|
|
||||||
|
Map<String, Object> capabilities = new HashMap<>();
|
||||||
|
capabilities.put("tools", Map.of());
|
||||||
|
capabilities.put("resources", Map.of());
|
||||||
|
capabilities.put("prompts", Map.of());
|
||||||
|
|
||||||
|
McpInitializeResponse.ServerInfo serverInfo = McpInitializeResponse.ServerInfo.builder()
|
||||||
|
.name(SERVER_NAME)
|
||||||
|
.version(SERVER_VERSION)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return McpInitializeResponse.builder()
|
||||||
|
.protocolVersion(PROTOCOL_VERSION)
|
||||||
|
.capabilities(capabilities)
|
||||||
|
.serverInfo(serverInfo)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public McpToolResponse.ToolListResponse listTools() {
|
||||||
|
log.info("MCP 도구 목록 조회");
|
||||||
|
|
||||||
|
List<McpToolResponse> tools = new ArrayList<>();
|
||||||
|
tools.add(createMemoryStoreTool());
|
||||||
|
tools.add(createMemoryRetrieveTool());
|
||||||
|
tools.add(createMemorySearchTool());
|
||||||
|
|
||||||
|
return McpToolResponse.ToolListResponse.builder()
|
||||||
|
.tools(tools)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public McpToolCallResponse callTool(String name, Object arguments) {
|
||||||
|
log.info("MCP 도구 호출: name={}, arguments={}", name, arguments);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> args = arguments instanceof Map ? (Map<String, Object>) arguments : new HashMap<>();
|
||||||
|
|
||||||
|
Map<String, Object> metadata = new HashMap<>();
|
||||||
|
metadata.put("tool", name);
|
||||||
|
metadata.put("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (name) {
|
||||||
|
case "store_memory" -> {
|
||||||
|
String userId = (String) args.get("user_id");
|
||||||
|
String memoryText = (String) args.get("memory_text");
|
||||||
|
Integer importance = args.get("importance") != null ?
|
||||||
|
((Number) args.get("importance")).intValue() : 1;
|
||||||
|
|
||||||
|
memoryService.storeTemporaryMemory(userId, memoryText, importance);
|
||||||
|
|
||||||
|
return McpToolCallResponse.builder()
|
||||||
|
.isError(false)
|
||||||
|
.content("Memory stored successfully")
|
||||||
|
.parts(Collections.emptyList())
|
||||||
|
.metadata(metadata)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
case "retrieve_memory" -> {
|
||||||
|
String userId = (String) args.get("user_id");
|
||||||
|
Integer limit = args.get("limit") != null ?
|
||||||
|
((Number) args.get("limit")).intValue() : 10;
|
||||||
|
|
||||||
|
var memories = memoryService.getPermanentMemories(userId, limit);
|
||||||
|
|
||||||
|
return McpToolCallResponse.builder()
|
||||||
|
.isError(false)
|
||||||
|
.content("Retrieved " + memories.size() + " memories")
|
||||||
|
.parts(Collections.emptyList())
|
||||||
|
.metadata(metadata)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
case "search_memory" -> {
|
||||||
|
String query = (String) args.get("query");
|
||||||
|
String userId = (String) args.get("user_id");
|
||||||
|
Integer limit = args.get("limit") != null ?
|
||||||
|
((Number) args.get("limit")).intValue() : 5;
|
||||||
|
|
||||||
|
var memories = memoryService.searchMemoriesByVector(query, userId, limit);
|
||||||
|
|
||||||
|
return McpToolCallResponse.builder()
|
||||||
|
.isError(false)
|
||||||
|
.content("Found " + memories.size() + " matching memories")
|
||||||
|
.parts(Collections.emptyList())
|
||||||
|
.metadata(metadata)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
return McpToolCallResponse.builder()
|
||||||
|
.isError(true)
|
||||||
|
.content("Unknown tool: " + name)
|
||||||
|
.parts(Collections.emptyList())
|
||||||
|
.metadata(metadata)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Tool execution error: {}", e.getMessage(), e);
|
||||||
|
return McpToolCallResponse.builder()
|
||||||
|
.isError(true)
|
||||||
|
.content("Error: " + e.getMessage())
|
||||||
|
.parts(Collections.emptyList())
|
||||||
|
.metadata(metadata)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public McpResourceResponse.ResourceListResponse listResources() {
|
||||||
|
log.info("MCP 리소스 목록 조회");
|
||||||
|
|
||||||
|
List<McpResourceResponse> resources = new ArrayList<>();
|
||||||
|
resources.add(McpResourceResponse.builder()
|
||||||
|
.uri("memory://recent")
|
||||||
|
.name("Recent Memories")
|
||||||
|
.description("최근 저장된 메모리 리소스")
|
||||||
|
.mimeType("application/json")
|
||||||
|
.build());
|
||||||
|
resources.add(McpResourceResponse.builder()
|
||||||
|
.uri("memory://long-term")
|
||||||
|
.name("Long-term Memories")
|
||||||
|
.description("장기 저장된 메모리 리소스")
|
||||||
|
.mimeType("application/json")
|
||||||
|
.build());
|
||||||
|
|
||||||
|
return McpResourceResponse.ResourceListResponse.builder()
|
||||||
|
.resources(resources)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public McpResourceResponse.ResourceReadResponse readResource(String uri) {
|
||||||
|
log.info("MCP 리소스 읽기: uri={}", uri);
|
||||||
|
|
||||||
|
Map<String, Object> metadata = new HashMap<>();
|
||||||
|
metadata.put("uri", uri);
|
||||||
|
metadata.put("timestamp", System.currentTimeMillis());
|
||||||
|
|
||||||
|
return McpResourceResponse.ResourceReadResponse.builder()
|
||||||
|
.uri(uri)
|
||||||
|
.mimeType("application/json")
|
||||||
|
.text("{\"message\": \"Resource content for " + uri + "\"}")
|
||||||
|
.metadata(metadata)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public McpPromptResponse.PromptListResponse listPrompts() {
|
||||||
|
log.info("MCP 프롬프트 목록 조회");
|
||||||
|
|
||||||
|
List<McpPromptResponse> prompts = new ArrayList<>();
|
||||||
|
prompts.add(createMemorySummaryPrompt());
|
||||||
|
prompts.add(createMemorySearchPrompt());
|
||||||
|
|
||||||
|
return McpPromptResponse.PromptListResponse.builder()
|
||||||
|
.prompts(prompts)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public McpPromptResponse.PromptGetResponse getPrompt(String name, Object arguments) {
|
||||||
|
log.info("MCP 프롬프트 가져오기: name={}, arguments={}", name, arguments);
|
||||||
|
|
||||||
|
List<McpPromptResponse.PromptMessage> messages = new ArrayList<>();
|
||||||
|
messages.add(McpPromptResponse.PromptMessage.builder()
|
||||||
|
.role("user")
|
||||||
|
.content("Prompt: " + name)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> args = arguments instanceof Map ? (Map<String, Object>) arguments : new HashMap<>();
|
||||||
|
|
||||||
|
return McpPromptResponse.PromptGetResponse.builder()
|
||||||
|
.name(name)
|
||||||
|
.messages(messages)
|
||||||
|
.arguments(args)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 도구 생성 헬퍼 메서드들
|
||||||
|
private McpToolResponse createMemoryStoreTool() {
|
||||||
|
Map<String, Object> inputSchema = new HashMap<>();
|
||||||
|
inputSchema.put("type", "object");
|
||||||
|
inputSchema.put("properties", Map.of(
|
||||||
|
"memory_text", Map.of("type", "string", "description", "저장할 메모리 텍스트"),
|
||||||
|
"user_id", Map.of("type", "string", "description", "사용자 ID"),
|
||||||
|
"importance", Map.of("type", "integer", "description", "중요도 (1-5)", "minimum", 1, "maximum", 5)
|
||||||
|
));
|
||||||
|
inputSchema.put("required", Arrays.asList("memory_text", "user_id"));
|
||||||
|
|
||||||
|
return McpToolResponse.builder()
|
||||||
|
.name("store_memory")
|
||||||
|
.description("메모리를 Redis에 임시 저장합니다")
|
||||||
|
.inputSchema(inputSchema)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private McpToolResponse createMemoryRetrieveTool() {
|
||||||
|
Map<String, Object> inputSchema = new HashMap<>();
|
||||||
|
inputSchema.put("type", "object");
|
||||||
|
inputSchema.put("properties", Map.of(
|
||||||
|
"user_id", Map.of("type", "string", "description", "사용자 ID"),
|
||||||
|
"limit", Map.of("type", "integer", "description", "조회할 메모리 개수", "default", 10)
|
||||||
|
));
|
||||||
|
inputSchema.put("required", Arrays.asList("user_id"));
|
||||||
|
|
||||||
|
return McpToolResponse.builder()
|
||||||
|
.name("retrieve_memory")
|
||||||
|
.description("사용자의 메모리를 조회합니다")
|
||||||
|
.inputSchema(inputSchema)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private McpToolResponse createMemorySearchTool() {
|
||||||
|
Map<String, Object> inputSchema = new HashMap<>();
|
||||||
|
inputSchema.put("type", "object");
|
||||||
|
inputSchema.put("properties", Map.of(
|
||||||
|
"query", Map.of("type", "string", "description", "검색 쿼리"),
|
||||||
|
"user_id", Map.of("type", "string", "description", "사용자 ID (선택)"),
|
||||||
|
"limit", Map.of("type", "integer", "description", "검색 결과 개수", "default", 5)
|
||||||
|
));
|
||||||
|
inputSchema.put("required", Arrays.asList("query"));
|
||||||
|
|
||||||
|
return McpToolResponse.builder()
|
||||||
|
.name("search_memory")
|
||||||
|
.description("벡터 유사도 기반으로 메모리를 검색합니다")
|
||||||
|
.inputSchema(inputSchema)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프롬프트 생성 헬퍼 메서드들
|
||||||
|
private McpPromptResponse createMemorySummaryPrompt() {
|
||||||
|
List<McpPromptResponse.PromptArgument> arguments = new ArrayList<>();
|
||||||
|
arguments.add(McpPromptResponse.PromptArgument.builder()
|
||||||
|
.name("memories")
|
||||||
|
.description("요약할 메모리 텍스트 목록")
|
||||||
|
.required(true)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
return McpPromptResponse.builder()
|
||||||
|
.name("memory_summary")
|
||||||
|
.description("메모리 목록을 요약하는 프롬프트")
|
||||||
|
.arguments(arguments)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private McpPromptResponse createMemorySearchPrompt() {
|
||||||
|
List<McpPromptResponse.PromptArgument> arguments = new ArrayList<>();
|
||||||
|
arguments.add(McpPromptResponse.PromptArgument.builder()
|
||||||
|
.name("query")
|
||||||
|
.description("검색할 키워드")
|
||||||
|
.required(true)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
return McpPromptResponse.builder()
|
||||||
|
.name("memory_search")
|
||||||
|
.description("메모리 검색을 위한 프롬프트")
|
||||||
|
.arguments(arguments)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.pandol365.dewey.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 서버 관련 설정
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class McpConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CORS 설정
|
||||||
|
* MCP 클라이언트가 다른 도메인에서 접근할 수 있도록 허용
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
|
registry.addMapping("/mcp/**")
|
||||||
|
.allowedOrigins("*")
|
||||||
|
.allowedMethods("GET", "POST", "OPTIONS")
|
||||||
|
.allowedHeaders("*")
|
||||||
|
.maxAge(3600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.pandol365.dewey.domain.memory.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 장기기억 엔티티 (Model)
|
||||||
|
* PostgreSQL에 영구 저장되는 정제된 메모리 데이터
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "memories")
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class Memory {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
@Column(nullable = false, columnDefinition = "TEXT")
|
||||||
|
private String memoryText;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer importance = 1; // 1~5
|
||||||
|
|
||||||
|
@Column(columnDefinition = "JSONB")
|
||||||
|
private String tags; // JSON 형식의 태그 배열
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private LocalDateTime createdAt = LocalDateTime.now();
|
||||||
|
|
||||||
|
@Column(name = "updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
// 벡터 임베딩은 별도 테이블 또는 pgvector 컬럼으로 관리
|
||||||
|
// Spring AI의 PgVectorStore를 사용할 경우 별도 처리
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.pandol365.dewey.domain.memory.model;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 임시 메모리 모델 (Model)
|
||||||
|
* Redis에 저장되는 임시 메모리 데이터를 표현
|
||||||
|
* 엔티티가 아닌 도메인 모델
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class TemporaryMemory {
|
||||||
|
|
||||||
|
private String id; // Redis key
|
||||||
|
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
private String memoryText;
|
||||||
|
|
||||||
|
private Integer importance;
|
||||||
|
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
private LocalDateTime expiresAt; // TTL 기반 만료 시간
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.pandol365.dewey.domain.memory.repository;
|
||||||
|
|
||||||
|
import com.pandol365.dewey.domain.memory.model.Memory;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memory 엔티티에 대한 데이터 접근 계층 (Repository)
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface MemoryRepository extends JpaRepository<Memory, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 ID로 메모리 조회
|
||||||
|
*/
|
||||||
|
List<Memory> findByUserIdOrderByCreatedAtDesc(String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 ID와 중요도로 메모리 조회
|
||||||
|
*/
|
||||||
|
List<Memory> findByUserIdAndImportanceGreaterThanEqualOrderByCreatedAtDesc(
|
||||||
|
String userId, Integer minImportance);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 ID로 최근 N개 메모리 조회
|
||||||
|
*/
|
||||||
|
@Query("SELECT m FROM Memory m WHERE m.userId = :userId ORDER BY m.createdAt DESC")
|
||||||
|
List<Memory> findRecentMemoriesByUserId(@Param("userId") String userId,
|
||||||
|
org.springframework.data.domain.Pageable pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.pandol365.dewey.domain.memory.service;
|
||||||
|
|
||||||
|
import com.pandol365.dewey.domain.memory.model.Memory;
|
||||||
|
import com.pandol365.dewey.domain.memory.model.TemporaryMemory;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메모리 도메인 비즈니스 로직 인터페이스 (Service)
|
||||||
|
*/
|
||||||
|
public interface MemoryService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 임시 메모리를 Redis에 저장
|
||||||
|
*/
|
||||||
|
TemporaryMemory storeTemporaryMemory(String userId, String memoryText, Integer importance);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자의 임시 메모리 조회
|
||||||
|
*/
|
||||||
|
List<TemporaryMemory> getTemporaryMemories(String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 임시 메모리를 영구 메모리로 저장 (PostgreSQL)
|
||||||
|
*/
|
||||||
|
Memory savePermanentMemory(Memory memory);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자의 영구 메모리 조회
|
||||||
|
*/
|
||||||
|
List<Memory> getPermanentMemories(String userId, Integer limit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 벡터 유사도 기반 메모리 검색
|
||||||
|
*/
|
||||||
|
List<Memory> searchMemoriesByVector(String query, String userId, Integer limit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 키워드 기반 메모리 검색
|
||||||
|
*/
|
||||||
|
List<Memory> searchMemoriesByKeyword(String keyword, String userId, Integer limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package com.pandol365.dewey.domain.memory.service.impl;
|
||||||
|
|
||||||
|
import com.pandol365.dewey.domain.memory.model.Memory;
|
||||||
|
import com.pandol365.dewey.domain.memory.model.TemporaryMemory;
|
||||||
|
import com.pandol365.dewey.domain.memory.repository.MemoryRepository;
|
||||||
|
import com.pandol365.dewey.domain.memory.service.MemoryService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메모리 도메인 비즈니스 로직 구현체 (Service)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional
|
||||||
|
public class MemoryServiceImpl implements MemoryService {
|
||||||
|
|
||||||
|
private final MemoryRepository memoryRepository;
|
||||||
|
// TODO: Redis 연동 추가 필요
|
||||||
|
// private final ReactiveRedisTemplate<String, TemporaryMemory> redisTemplate;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TemporaryMemory storeTemporaryMemory(String userId, String memoryText, Integer importance) {
|
||||||
|
log.info("임시 메모리 저장: userId={}, importance={}", userId, importance);
|
||||||
|
|
||||||
|
// TODO: Redis에 저장하는 로직 구현
|
||||||
|
TemporaryMemory temporaryMemory = TemporaryMemory.builder()
|
||||||
|
.id(UUID.randomUUID().toString())
|
||||||
|
.userId(userId)
|
||||||
|
.memoryText(memoryText)
|
||||||
|
.importance(importance != null ? importance : 1)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.expiresAt(LocalDateTime.now().plusDays(3)) // TTL 3일
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// TODO: redisTemplate.opsForValue().set(key, temporaryMemory, Duration.ofDays(3));
|
||||||
|
|
||||||
|
return temporaryMemory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<TemporaryMemory> getTemporaryMemories(String userId) {
|
||||||
|
log.info("임시 메모리 조회: userId={}", userId);
|
||||||
|
|
||||||
|
// TODO: Redis에서 조회하는 로직 구현
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Memory savePermanentMemory(Memory memory) {
|
||||||
|
log.info("영구 메모리 저장: userId={}, importance={}", memory.getUserId(), memory.getImportance());
|
||||||
|
|
||||||
|
memory.setUpdatedAt(LocalDateTime.now());
|
||||||
|
return memoryRepository.save(memory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<Memory> getPermanentMemories(String userId, Integer limit) {
|
||||||
|
log.info("영구 메모리 조회: userId={}, limit={}", userId, limit);
|
||||||
|
|
||||||
|
PageRequest pageRequest = PageRequest.of(0, limit != null ? limit : 10);
|
||||||
|
return memoryRepository.findRecentMemoriesByUserId(userId, pageRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<Memory> searchMemoriesByVector(String query, String userId, Integer limit) {
|
||||||
|
log.info("벡터 기반 메모리 검색: query={}, userId={}, limit={}", query, userId, limit);
|
||||||
|
|
||||||
|
// TODO: 벡터 유사도 검색 구현 (Spring AI PgVectorStore 사용)
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<Memory> searchMemoriesByKeyword(String keyword, String userId, Integer limit) {
|
||||||
|
log.info("키워드 기반 메모리 검색: keyword={}, userId={}, limit={}", keyword, userId, limit);
|
||||||
|
|
||||||
|
// TODO: 키워드 검색 구현 (LIKE 또는 Full-Text Search)
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
package com.pandol365.dewey.exception;
|
||||||
|
|
||||||
|
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.validation.FieldError;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 글로벌 예외 처리 핸들러
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 유효성 검증 예외 처리
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleValidationExceptions(
|
||||||
|
MethodArgumentNotValidException ex) {
|
||||||
|
Map<String, String> errors = new HashMap<>();
|
||||||
|
ex.getBindingResult().getAllErrors().forEach((error) -> {
|
||||||
|
String fieldName = ((FieldError) error).getField();
|
||||||
|
String errorMessage = error.getDefaultMessage();
|
||||||
|
errors.put(fieldName, errorMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("errors", errors);
|
||||||
|
response.put("message", "Validation failed");
|
||||||
|
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IllegalArgumentException 처리
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
public ResponseEntity<McpJsonRpcResponse> handleIllegalArgumentException(
|
||||||
|
IllegalArgumentException ex) {
|
||||||
|
log.error("Illegal argument exception: {}", ex.getMessage());
|
||||||
|
|
||||||
|
McpJsonRpcResponse.McpError error = McpJsonRpcResponse.McpError.builder()
|
||||||
|
.code(-32602) // Invalid params
|
||||||
|
.message(ex.getMessage())
|
||||||
|
.data(null)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
McpJsonRpcResponse response = McpJsonRpcResponse.builder()
|
||||||
|
.jsonrpc("2.0")
|
||||||
|
.error(error)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일반 예외 처리
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<McpJsonRpcResponse> handleException(Exception ex) {
|
||||||
|
log.error("Unexpected exception: ", ex);
|
||||||
|
|
||||||
|
McpJsonRpcResponse.McpError error = McpJsonRpcResponse.McpError.builder()
|
||||||
|
.code(-32603) // Internal error
|
||||||
|
.message(ex.getMessage())
|
||||||
|
.data(null)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
McpJsonRpcResponse response = McpJsonRpcResponse.builder()
|
||||||
|
.jsonrpc("2.0")
|
||||||
|
.error(error)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1 +1,17 @@
|
||||||
spring.application.name=dewey
|
spring.application.name=dewey
|
||||||
|
|
||||||
|
# PostgreSQL
|
||||||
|
spring.datasource.url=jdbc:postgresql://localhost:5432/dewey_memory
|
||||||
|
spring.datasource.username=dewey
|
||||||
|
spring.datasource.password=0bk1rWu98mGl5ea3
|
||||||
|
spring.datasource.driver-class-name=org.postgresql.Driver
|
||||||
|
|
||||||
|
# JPA
|
||||||
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
|
spring.jpa.show-sql=true
|
||||||
|
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
spring.jpa.properties.hibernate.format_sql=true
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
spring.data.redis.host=localhost
|
||||||
|
spring.data.redis.port=6379
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue