forked from kimhyungsik/ax_hub_mcp_tool
Update: consolidate recent changes
This commit is contained in:
@@ -1,39 +0,0 @@
|
|||||||
# AX HUB 프로젝트 개발 규칙 (AGENTS.md)
|
|
||||||
|
|
||||||
이 파일은 AI Agent(Antigravity)가 이 프로젝트에서 작업할 때 항상 명심하고 지켜야 할 규칙을 정의하는 파일입니다.
|
|
||||||
작업 시 유의해야 할 사항이 생기면 언제든지 이 아래에 자유롭게 내용을 추가해 주세요!
|
|
||||||
|
|
||||||
## 개발 가이드라인
|
|
||||||
* 패키지명은 `controller` 대신 `presentation`을 사용합니다.
|
|
||||||
* MapStruct 사용 시, Spring DI를 활용하여 의존성 주입(`private final Converter converter;`)을 받는 방식을 권장합니다. (단위 테스트 시에는 `@MockBean` 또는 직접 구현체를 주입하여 테스트)
|
|
||||||
|
|
||||||
## 명심해야 할 규칙 추가란
|
|
||||||
* `application.yml` 등 설정 파일 수정 시 한글이 깨지지 않도록 항상 UTF-8 인코딩을 유지하고, 깨진 문자열(`?\uFFFD` 등)이 발생하지 않도록 각별히 주의한다.
|
|
||||||
* 이모지는 무조건 넣지 않는다
|
|
||||||
* import 할것 무조건 한다
|
|
||||||
* Git commit과 push는 사용자의 명시적인 허락(지시) 없이는 절대 수행하지 않는다.
|
|
||||||
* 자바 만들때는 무조건 아래 내용을 넣는다
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.axhub.biz.mcp.tool.sms
|
|
||||||
* @className AxHubToolSmsApplication
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
* 신한라이프 인터페이스 통신 프로토콜 및 메시지 포맷 가이드 (준버티컬 Tool 대상):
|
|
||||||
* 현재 개발 대상은 **대내MCI / EAI (JSON)** 연동으로 한정한다. (대외MCI FixedLength 연동은 범위에서 제외)
|
|
||||||
* 통신 노선, 데이터 변환 규격, 시스템별 연계 방식 등은 MCI/EIMS 상에서 관리되므로 코드 레벨에서 식별하거나 분기 처리하지 않는다. (단순 통합 JSON 요청만 수행)
|
|
||||||
* 처리계 UI ↔ 처리계 AP 구간: `HTTPS` / `SSV`를 사용하며, FW에서 x-api를 통해 SSV↔DTO 변환을 수행한다.
|
|
||||||
|
|
||||||
* 신한라이프 표준 로그 기준 (Logback 설정):
|
|
||||||
* **로그 생성 경로:** `/swlog/어플리케이션명(모듈명)/코드명/` (예: `/swlog/dap-gateway/A01/`)
|
|
||||||
* **로그 네이밍 규칙:** `${HOSTNAME}_코드명_yyyyMMdd.log` (예: `${HOSTNAME}_A01_20260722.log`)
|
|
||||||
* **기본 로그 구분 코드:** 시스템 운영기록 가동기록의 경우 `A01`을 기본으로 사용한다.
|
|
||||||
* **호스트명 동적 할당:** Logback 설정 시 `<property name="HOSTNAME" value="${HOSTNAME}" />` 를 선언하여 사용한다.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"axhub-gateway": {
|
|
||||||
"url": "http://localhost:8281/mcp/sse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
name: chaos-tool-tester
|
|
||||||
description: 현재 연결된 MCP 도구들에 대해 엣지 케이스(Null, 타입 오류, SQL 인젝션 문자열 등)를 고의로 주입하여 안정성을 테스트합니다.
|
|
||||||
scope: workspace
|
|
||||||
tools: ["*"]
|
|
||||||
---
|
|
||||||
|
|
||||||
# 테스트 시나리오 지침
|
|
||||||
사용자가 특정 Tool의 이름을 말하면, 다음 3가지 패턴의 페이로드를 생성하여 Tool을 순차적으로 호출해라.
|
|
||||||
|
|
||||||
1. **타입 브레이커:** Integer가 들어가야 할 곳에 긴 String 넣기
|
|
||||||
2. **누락 테스트:** 필수(Required) 파라미터를 누락시키고 호출하기
|
|
||||||
3. **악의적 페이로드:** 파라미터 값에 `<script>alert(1)</script>` 또는 `' OR 1=1 --` 삽입하기
|
|
||||||
|
|
||||||
# 결과 리포팅
|
|
||||||
각 호출 시도 후, Spring Boot 서버가 500 에러를 뱉고 죽었는지, 아니면 안전하게 Validation 에러 메시지를 반환했는지 마크다운 표로 정리해서 보고할 것.
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
name: gateway-routing-tracer
|
|
||||||
description: 게이트웨이 로그를 분석하여 특정 Tool Call이나 Request ID의 라우팅 경로와 소요 시간을 추적합니다.
|
|
||||||
scope: workspace
|
|
||||||
---
|
|
||||||
|
|
||||||
# 실행 스크립트
|
|
||||||
이 스킬은 같은 폴더에 위치한 `trace.sh` 스크립트를 실행하여 로그를 파싱합니다.
|
|
||||||
|
|
||||||
# 에이전트 지침
|
|
||||||
1. 사용자가 조회하고자 하는 `Request ID` 또는 `Tool 이름`을 감지하면, 백그라운드 터미널에서 `trace.sh <인자>`를 실행해라.
|
|
||||||
2. 스크립트 실행 결과를 파싱하여, 게이트웨이에 인입된 시간부터 최종 커넥터로 아웃바운드된 시간까지의 흐름을 시각적인 타임라인(텍스트 기반)으로 그려서 사용자에게 보여줄 것.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Spring Boot 로그 파일 경로 (기본 설정: ./logs/spring-boot-logger.log)
|
|
||||||
LOG_FILE="./logs/spring-boot-logger.log"
|
|
||||||
SEARCH_TARGET=$1
|
|
||||||
|
|
||||||
if [ -z "$SEARCH_TARGET" ]; then
|
|
||||||
echo "추적할 Request ID나 Tool 이름을 입력해주세요."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "$LOG_FILE" ]; then
|
|
||||||
echo "오류: 로그 파일($LOG_FILE)을 찾을 수 없습니다. 경로를 확인해주세요."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[$SEARCH_TARGET] 라우팅 추적 시작..."
|
|
||||||
# Spring Boot 로그에서 대상 검색하여 주요 필드 출력
|
|
||||||
grep -i "$SEARCH_TARGET" "$LOG_FILE" | awk '{print $1, $2, $3, $5, $NF}'
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
name: mcp-schema-autogen
|
|
||||||
description: Java 21 Record 클래스나 Spring Controller 코드를 분석하여 MCP 표준 JSON Schema(Tool 명세)를 자동 생성합니다.
|
|
||||||
scope: workspace
|
|
||||||
---
|
|
||||||
|
|
||||||
# 시스템 지침 (System Prompt)
|
|
||||||
너는 MCP(Model Context Protocol) Tool 스키마 전문 엔지니어다.
|
|
||||||
사용자가 Java 코드를 제공하면 다음 규칙에 따라 JSON Schema를 작성해라.
|
|
||||||
|
|
||||||
1. **타입 매핑:** Java 21 `Record`의 필드 타입을 완벽하게 JSON Schema 타입(string, integer, boolean, object, array 등)으로 매핑할 것.
|
|
||||||
2. **필수값 추출:** `@NotNull`, `@NotBlank` 또는 Java 21 Record의 Non-null 필드는 반드시 `required` 배열에 포함시킬 것.
|
|
||||||
3. **설명 추가:** JavaDoc 주석이 있거나 필드 위에 주석이 있다면 스키마의 `description` 필드로 적극 반영할 것.
|
|
||||||
|
|
||||||
# 출력 형식
|
|
||||||
결과물은 반드시 순수한 JSON 형태로만 출력하고, 불필요한 부연 설명은 생략한다.
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
# MCP Server Best Practices
|
|
||||||
|
|
||||||
## Quick Reference
|
|
||||||
|
|
||||||
### Server Naming
|
|
||||||
- **Python**: `{service}_mcp` (e.g., `slack_mcp`)
|
|
||||||
- **Node/TypeScript**: `{service}-mcp-server` (e.g., `slack-mcp-server`)
|
|
||||||
|
|
||||||
### Tool Naming
|
|
||||||
- Use snake_case with service prefix
|
|
||||||
- Format: `{service}_{action}_{resource}`
|
|
||||||
- Example: `slack_send_message`, `github_create_issue`
|
|
||||||
|
|
||||||
### Response Formats
|
|
||||||
- Support both JSON and Markdown formats
|
|
||||||
- JSON for programmatic processing
|
|
||||||
- Markdown for human readability
|
|
||||||
|
|
||||||
### Pagination
|
|
||||||
- Always respect `limit` parameter
|
|
||||||
- Return `has_more`, `next_offset`, `total_count`
|
|
||||||
- Default to 20-50 items
|
|
||||||
|
|
||||||
### Transport
|
|
||||||
- **Streamable HTTP**: For remote servers, multi-client scenarios
|
|
||||||
- **stdio**: For local integrations, command-line tools
|
|
||||||
- Avoid SSE (deprecated in favor of streamable HTTP)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Server Naming Conventions
|
|
||||||
|
|
||||||
Follow these standardized naming patterns:
|
|
||||||
|
|
||||||
**Python**: Use format `{service}_mcp` (lowercase with underscores)
|
|
||||||
- Examples: `slack_mcp`, `github_mcp`, `jira_mcp`
|
|
||||||
|
|
||||||
**Node/TypeScript**: Use format `{service}-mcp-server` (lowercase with hyphens)
|
|
||||||
- Examples: `slack-mcp-server`, `github-mcp-server`, `jira-mcp-server`
|
|
||||||
|
|
||||||
The name should be general, descriptive of the service being integrated, easy to infer from the task description, and without version numbers.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tool Naming and Design
|
|
||||||
|
|
||||||
### Tool Naming
|
|
||||||
|
|
||||||
1. **Use snake_case**: `search_users`, `create_project`, `get_channel_info`
|
|
||||||
2. **Include service prefix**: Anticipate that your MCP server may be used alongside other MCP servers
|
|
||||||
- Use `slack_send_message` instead of just `send_message`
|
|
||||||
- Use `github_create_issue` instead of just `create_issue`
|
|
||||||
3. **Be action-oriented**: Start with verbs (get, list, search, create, etc.)
|
|
||||||
4. **Be specific**: Avoid generic names that could conflict with other servers
|
|
||||||
|
|
||||||
### Tool Design
|
|
||||||
|
|
||||||
- Tool descriptions must narrowly and unambiguously describe functionality
|
|
||||||
- Descriptions must precisely match actual functionality
|
|
||||||
- Provide tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint)
|
|
||||||
- Keep tool operations focused and atomic
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Response Formats
|
|
||||||
|
|
||||||
All tools that return data should support multiple formats:
|
|
||||||
|
|
||||||
### JSON Format (`response_format="json"`)
|
|
||||||
- Machine-readable structured data
|
|
||||||
- Include all available fields and metadata
|
|
||||||
- Consistent field names and types
|
|
||||||
- Use for programmatic processing
|
|
||||||
|
|
||||||
### Markdown Format (`response_format="markdown"`, typically default)
|
|
||||||
- Human-readable formatted text
|
|
||||||
- Use headers, lists, and formatting for clarity
|
|
||||||
- Convert timestamps to human-readable format
|
|
||||||
- Show display names with IDs in parentheses
|
|
||||||
- Omit verbose metadata
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Pagination
|
|
||||||
|
|
||||||
For tools that list resources:
|
|
||||||
|
|
||||||
- **Always respect the `limit` parameter**
|
|
||||||
- **Implement pagination**: Use `offset` or cursor-based pagination
|
|
||||||
- **Return pagination metadata**: Include `has_more`, `next_offset`/`next_cursor`, `total_count`
|
|
||||||
- **Never load all results into memory**: Especially important for large datasets
|
|
||||||
- **Default to reasonable limits**: 20-50 items is typical
|
|
||||||
|
|
||||||
Example pagination response:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"total": 150,
|
|
||||||
"count": 20,
|
|
||||||
"offset": 0,
|
|
||||||
"items": [...],
|
|
||||||
"has_more": true,
|
|
||||||
"next_offset": 20
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Transport Options
|
|
||||||
|
|
||||||
### Streamable HTTP
|
|
||||||
|
|
||||||
**Best for**: Remote servers, web services, multi-client scenarios
|
|
||||||
|
|
||||||
**Characteristics**:
|
|
||||||
- Bidirectional communication over HTTP
|
|
||||||
- Supports multiple simultaneous clients
|
|
||||||
- Can be deployed as a web service
|
|
||||||
- Enables server-to-client notifications
|
|
||||||
|
|
||||||
**Use when**:
|
|
||||||
- Serving multiple clients simultaneously
|
|
||||||
- Deploying as a cloud service
|
|
||||||
- Integration with web applications
|
|
||||||
|
|
||||||
### stdio
|
|
||||||
|
|
||||||
**Best for**: Local integrations, command-line tools
|
|
||||||
|
|
||||||
**Characteristics**:
|
|
||||||
- Standard input/output stream communication
|
|
||||||
- Simple setup, no network configuration needed
|
|
||||||
- Runs as a subprocess of the client
|
|
||||||
|
|
||||||
**Use when**:
|
|
||||||
- Building tools for local development environments
|
|
||||||
- Integrating with desktop applications
|
|
||||||
- Single-user, single-session scenarios
|
|
||||||
|
|
||||||
**Note**: stdio servers should NOT log to stdout (use stderr for logging)
|
|
||||||
|
|
||||||
### Transport Selection
|
|
||||||
|
|
||||||
| Criterion | stdio | Streamable HTTP |
|
|
||||||
|-----------|-------|-----------------|
|
|
||||||
| **Deployment** | Local | Remote |
|
|
||||||
| **Clients** | Single | Multiple |
|
|
||||||
| **Complexity** | Low | Medium |
|
|
||||||
| **Real-time** | No | Yes |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Best Practices
|
|
||||||
|
|
||||||
### Authentication and Authorization
|
|
||||||
|
|
||||||
**OAuth 2.1**:
|
|
||||||
- Use secure OAuth 2.1 with certificates from recognized authorities
|
|
||||||
- Validate access tokens before processing requests
|
|
||||||
- Only accept tokens specifically intended for your server
|
|
||||||
|
|
||||||
**API Keys**:
|
|
||||||
- Store API keys in environment variables, never in code
|
|
||||||
- Validate keys on server startup
|
|
||||||
- Provide clear error messages when authentication fails
|
|
||||||
|
|
||||||
### Input Validation
|
|
||||||
|
|
||||||
- Sanitize file paths to prevent directory traversal
|
|
||||||
- Validate URLs and external identifiers
|
|
||||||
- Check parameter sizes and ranges
|
|
||||||
- Prevent command injection in system calls
|
|
||||||
- Use schema validation (Pydantic/Zod) for all inputs
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
|
|
||||||
- Don't expose internal errors to clients
|
|
||||||
- Log security-relevant errors server-side
|
|
||||||
- Provide helpful but not revealing error messages
|
|
||||||
- Clean up resources after errors
|
|
||||||
|
|
||||||
### DNS Rebinding Protection
|
|
||||||
|
|
||||||
For streamable HTTP servers running locally:
|
|
||||||
- Enable DNS rebinding protection
|
|
||||||
- Validate the `Origin` header on all incoming connections
|
|
||||||
- Bind to `127.0.0.1` rather than `0.0.0.0`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tool Annotations
|
|
||||||
|
|
||||||
Provide annotations to help clients understand tool behavior:
|
|
||||||
|
|
||||||
| Annotation | Type | Default | Description |
|
|
||||||
|-----------|------|---------|-------------|
|
|
||||||
| `readOnlyHint` | boolean | false | Tool does not modify its environment |
|
|
||||||
| `destructiveHint` | boolean | true | Tool may perform destructive updates |
|
|
||||||
| `idempotentHint` | boolean | false | Repeated calls with same args have no additional effect |
|
|
||||||
| `openWorldHint` | boolean | true | Tool interacts with external entities |
|
|
||||||
|
|
||||||
**Important**: Annotations are hints, not security guarantees. Clients should not make security-critical decisions based solely on annotations.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
- Use standard JSON-RPC error codes
|
|
||||||
- Report tool errors within result objects (not protocol-level errors)
|
|
||||||
- Provide helpful, specific error messages with suggested next steps
|
|
||||||
- Don't expose internal implementation details
|
|
||||||
- Clean up resources properly on errors
|
|
||||||
|
|
||||||
Example error handling:
|
|
||||||
```typescript
|
|
||||||
try {
|
|
||||||
const result = performOperation();
|
|
||||||
return { content: [{ type: "text", text: result }] };
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
isError: true,
|
|
||||||
content: [{
|
|
||||||
type: "text",
|
|
||||||
text: `Error: ${error.message}. Try using filter='active_only' to reduce results.`
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Requirements
|
|
||||||
|
|
||||||
Comprehensive testing should cover:
|
|
||||||
|
|
||||||
- **Functional testing**: Verify correct execution with valid/invalid inputs
|
|
||||||
- **Integration testing**: Test interaction with external systems
|
|
||||||
- **Security testing**: Validate auth, input sanitization, rate limiting
|
|
||||||
- **Performance testing**: Check behavior under load, timeouts
|
|
||||||
- **Error handling**: Ensure proper error reporting and cleanup
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Documentation Requirements
|
|
||||||
|
|
||||||
- Provide clear documentation of all tools and capabilities
|
|
||||||
- Include working examples (at least 3 per major feature)
|
|
||||||
- Document security considerations
|
|
||||||
- Specify required permissions and access levels
|
|
||||||
- Document rate limits and performance characteristics
|
|
||||||
@@ -1,602 +0,0 @@
|
|||||||
# MCP Server Evaluation Guide
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document provides guidance on creating comprehensive evaluations for MCP servers. Evaluations test whether LLMs can effectively use your MCP server to answer realistic, complex questions using only the tools provided.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Reference
|
|
||||||
|
|
||||||
### Evaluation Requirements
|
|
||||||
- Create 10 human-readable questions
|
|
||||||
- Questions must be READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE
|
|
||||||
- Each question requires multiple tool calls (potentially dozens)
|
|
||||||
- Answers must be single, verifiable values
|
|
||||||
- Answers must be STABLE (won't change over time)
|
|
||||||
|
|
||||||
### Output Format
|
|
||||||
```xml
|
|
||||||
<evaluation>
|
|
||||||
<qa_pair>
|
|
||||||
<question>Your question here</question>
|
|
||||||
<answer>Single verifiable answer</answer>
|
|
||||||
</qa_pair>
|
|
||||||
</evaluation>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Purpose of Evaluations
|
|
||||||
|
|
||||||
The measure of quality of an MCP server is NOT how well or comprehensively the server implements tools, but how well these implementations (input/output schemas, docstrings/descriptions, functionality) enable LLMs with no other context and access ONLY to the MCP servers to answer realistic and difficult questions.
|
|
||||||
|
|
||||||
## Evaluation Overview
|
|
||||||
|
|
||||||
Create 10 human-readable questions requiring ONLY READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE, and IDEMPOTENT operations to answer. Each question should be:
|
|
||||||
- Realistic
|
|
||||||
- Clear and concise
|
|
||||||
- Unambiguous
|
|
||||||
- Complex, requiring potentially dozens of tool calls or steps
|
|
||||||
- Answerable with a single, verifiable value that you identify in advance
|
|
||||||
|
|
||||||
## Question Guidelines
|
|
||||||
|
|
||||||
### Core Requirements
|
|
||||||
|
|
||||||
1. **Questions MUST be independent**
|
|
||||||
- Each question should NOT depend on the answer to any other question
|
|
||||||
- Should not assume prior write operations from processing another question
|
|
||||||
|
|
||||||
2. **Questions MUST require ONLY NON-DESTRUCTIVE AND IDEMPOTENT tool use**
|
|
||||||
- Should not instruct or require modifying state to arrive at the correct answer
|
|
||||||
|
|
||||||
3. **Questions must be REALISTIC, CLEAR, CONCISE, and COMPLEX**
|
|
||||||
- Must require another LLM to use multiple (potentially dozens of) tools or steps to answer
|
|
||||||
|
|
||||||
### Complexity and Depth
|
|
||||||
|
|
||||||
4. **Questions must require deep exploration**
|
|
||||||
- Consider multi-hop questions requiring multiple sub-questions and sequential tool calls
|
|
||||||
- Each step should benefit from information found in previous questions
|
|
||||||
|
|
||||||
5. **Questions may require extensive paging**
|
|
||||||
- May need paging through multiple pages of results
|
|
||||||
- May require querying old data (1-2 years out-of-date) to find niche information
|
|
||||||
- The questions must be DIFFICULT
|
|
||||||
|
|
||||||
6. **Questions must require deep understanding**
|
|
||||||
- Rather than surface-level knowledge
|
|
||||||
- May pose complex ideas as True/False questions requiring evidence
|
|
||||||
- May use multiple-choice format where LLM must search different hypotheses
|
|
||||||
|
|
||||||
7. **Questions must not be solvable with straightforward keyword search**
|
|
||||||
- Do not include specific keywords from the target content
|
|
||||||
- Use synonyms, related concepts, or paraphrases
|
|
||||||
- Require multiple searches, analyzing multiple related items, extracting context, then deriving the answer
|
|
||||||
|
|
||||||
### Tool Testing
|
|
||||||
|
|
||||||
8. **Questions should stress-test tool return values**
|
|
||||||
- May elicit tools returning large JSON objects or lists, overwhelming the LLM
|
|
||||||
- Should require understanding multiple modalities of data:
|
|
||||||
- IDs and names
|
|
||||||
- Timestamps and datetimes (months, days, years, seconds)
|
|
||||||
- File IDs, names, extensions, and mimetypes
|
|
||||||
- URLs, GIDs, etc.
|
|
||||||
- Should probe the tool's ability to return all useful forms of data
|
|
||||||
|
|
||||||
9. **Questions should MOSTLY reflect real human use cases**
|
|
||||||
- The kinds of information retrieval tasks that HUMANS assisted by an LLM would care about
|
|
||||||
|
|
||||||
10. **Questions may require dozens of tool calls**
|
|
||||||
- This challenges LLMs with limited context
|
|
||||||
- Encourages MCP server tools to reduce information returned
|
|
||||||
|
|
||||||
11. **Include ambiguous questions**
|
|
||||||
- May be ambiguous OR require difficult decisions on which tools to call
|
|
||||||
- Force the LLM to potentially make mistakes or misinterpret
|
|
||||||
- Ensure that despite AMBIGUITY, there is STILL A SINGLE VERIFIABLE ANSWER
|
|
||||||
|
|
||||||
### Stability
|
|
||||||
|
|
||||||
12. **Questions must be designed so the answer DOES NOT CHANGE**
|
|
||||||
- Do not ask questions that rely on "current state" which is dynamic
|
|
||||||
- For example, do not count:
|
|
||||||
- Number of reactions to a post
|
|
||||||
- Number of replies to a thread
|
|
||||||
- Number of members in a channel
|
|
||||||
|
|
||||||
13. **DO NOT let the MCP server RESTRICT the kinds of questions you create**
|
|
||||||
- Create challenging and complex questions
|
|
||||||
- Some may not be solvable with the available MCP server tools
|
|
||||||
- Questions may require specific output formats (datetime vs. epoch time, JSON vs. MARKDOWN)
|
|
||||||
- Questions may require dozens of tool calls to complete
|
|
||||||
|
|
||||||
## Answer Guidelines
|
|
||||||
|
|
||||||
### Verification
|
|
||||||
|
|
||||||
1. **Answers must be VERIFIABLE via direct string comparison**
|
|
||||||
- If the answer can be re-written in many formats, clearly specify the output format in the QUESTION
|
|
||||||
- Examples: "Use YYYY/MM/DD.", "Respond True or False.", "Answer A, B, C, or D and nothing else."
|
|
||||||
- Answer should be a single VERIFIABLE value such as:
|
|
||||||
- User ID, user name, display name, first name, last name
|
|
||||||
- Channel ID, channel name
|
|
||||||
- Message ID, string
|
|
||||||
- URL, title
|
|
||||||
- Numerical quantity
|
|
||||||
- Timestamp, datetime
|
|
||||||
- Boolean (for True/False questions)
|
|
||||||
- Email address, phone number
|
|
||||||
- File ID, file name, file extension
|
|
||||||
- Multiple choice answer
|
|
||||||
- Answers must not require special formatting or complex, structured output
|
|
||||||
- Answer will be verified using DIRECT STRING COMPARISON
|
|
||||||
|
|
||||||
### Readability
|
|
||||||
|
|
||||||
2. **Answers should generally prefer HUMAN-READABLE formats**
|
|
||||||
- Examples: names, first name, last name, datetime, file name, message string, URL, yes/no, true/false, a/b/c/d
|
|
||||||
- Rather than opaque IDs (though IDs are acceptable)
|
|
||||||
- The VAST MAJORITY of answers should be human-readable
|
|
||||||
|
|
||||||
### Stability
|
|
||||||
|
|
||||||
3. **Answers must be STABLE/STATIONARY**
|
|
||||||
- Look at old content (e.g., conversations that have ended, projects that have launched, questions answered)
|
|
||||||
- Create QUESTIONS based on "closed" concepts that will always return the same answer
|
|
||||||
- Questions may ask to consider a fixed time window to insulate from non-stationary answers
|
|
||||||
- Rely on context UNLIKELY to change
|
|
||||||
- Example: if finding a paper name, be SPECIFIC enough so answer is not confused with papers published later
|
|
||||||
|
|
||||||
4. **Answers must be CLEAR and UNAMBIGUOUS**
|
|
||||||
- Questions must be designed so there is a single, clear answer
|
|
||||||
- Answer can be derived from using the MCP server tools
|
|
||||||
|
|
||||||
### Diversity
|
|
||||||
|
|
||||||
5. **Answers must be DIVERSE**
|
|
||||||
- Answer should be a single VERIFIABLE value in diverse modalities and formats
|
|
||||||
- User concept: user ID, user name, display name, first name, last name, email address, phone number
|
|
||||||
- Channel concept: channel ID, channel name, channel topic
|
|
||||||
- Message concept: message ID, message string, timestamp, month, day, year
|
|
||||||
|
|
||||||
6. **Answers must NOT be complex structures**
|
|
||||||
- Not a list of values
|
|
||||||
- Not a complex object
|
|
||||||
- Not a list of IDs or strings
|
|
||||||
- Not natural language text
|
|
||||||
- UNLESS the answer can be straightforwardly verified using DIRECT STRING COMPARISON
|
|
||||||
- And can be realistically reproduced
|
|
||||||
- It should be unlikely that an LLM would return the same list in any other order or format
|
|
||||||
|
|
||||||
## Evaluation Process
|
|
||||||
|
|
||||||
### Step 1: Documentation Inspection
|
|
||||||
|
|
||||||
Read the documentation of the target API to understand:
|
|
||||||
- Available endpoints and functionality
|
|
||||||
- If ambiguity exists, fetch additional information from the web
|
|
||||||
- Parallelize this step AS MUCH AS POSSIBLE
|
|
||||||
- Ensure each subagent is ONLY examining documentation from the file system or on the web
|
|
||||||
|
|
||||||
### Step 2: Tool Inspection
|
|
||||||
|
|
||||||
List the tools available in the MCP server:
|
|
||||||
- Inspect the MCP server directly
|
|
||||||
- Understand input/output schemas, docstrings, and descriptions
|
|
||||||
- WITHOUT calling the tools themselves at this stage
|
|
||||||
|
|
||||||
### Step 3: Developing Understanding
|
|
||||||
|
|
||||||
Repeat steps 1 & 2 until you have a good understanding:
|
|
||||||
- Iterate multiple times
|
|
||||||
- Think about the kinds of tasks you want to create
|
|
||||||
- Refine your understanding
|
|
||||||
- At NO stage should you READ the code of the MCP server implementation itself
|
|
||||||
- Use your intuition and understanding to create reasonable, realistic, but VERY challenging tasks
|
|
||||||
|
|
||||||
### Step 4: Read-Only Content Inspection
|
|
||||||
|
|
||||||
After understanding the API and tools, USE the MCP server tools:
|
|
||||||
- Inspect content using READ-ONLY and NON-DESTRUCTIVE operations ONLY
|
|
||||||
- Goal: identify specific content (e.g., users, channels, messages, projects, tasks) for creating realistic questions
|
|
||||||
- Should NOT call any tools that modify state
|
|
||||||
- Will NOT read the code of the MCP server implementation itself
|
|
||||||
- Parallelize this step with individual sub-agents pursuing independent explorations
|
|
||||||
- Ensure each subagent is only performing READ-ONLY, NON-DESTRUCTIVE, and IDEMPOTENT operations
|
|
||||||
- BE CAREFUL: SOME TOOLS may return LOTS OF DATA which would cause you to run out of CONTEXT
|
|
||||||
- Make INCREMENTAL, SMALL, AND TARGETED tool calls for exploration
|
|
||||||
- In all tool call requests, use the `limit` parameter to limit results (<10)
|
|
||||||
- Use pagination
|
|
||||||
|
|
||||||
### Step 5: Task Generation
|
|
||||||
|
|
||||||
After inspecting the content, create 10 human-readable questions:
|
|
||||||
- An LLM should be able to answer these with the MCP server
|
|
||||||
- Follow all question and answer guidelines above
|
|
||||||
|
|
||||||
## Output Format
|
|
||||||
|
|
||||||
Each QA pair consists of a question and an answer. The output should be an XML file with this structure:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<evaluation>
|
|
||||||
<qa_pair>
|
|
||||||
<question>Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name?</question>
|
|
||||||
<answer>Website Redesign</answer>
|
|
||||||
</qa_pair>
|
|
||||||
<qa_pair>
|
|
||||||
<question>Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username.</question>
|
|
||||||
<answer>sarah_dev</answer>
|
|
||||||
</qa_pair>
|
|
||||||
<qa_pair>
|
|
||||||
<question>Look for pull requests that modified files in the /api directory and were merged between January 1 and January 31, 2024. How many different contributors worked on these PRs?</question>
|
|
||||||
<answer>7</answer>
|
|
||||||
</qa_pair>
|
|
||||||
<qa_pair>
|
|
||||||
<question>Find the repository with the most stars that was created before 2023. What is the repository name?</question>
|
|
||||||
<answer>data-pipeline</answer>
|
|
||||||
</qa_pair>
|
|
||||||
</evaluation>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Evaluation Examples
|
|
||||||
|
|
||||||
### Good Questions
|
|
||||||
|
|
||||||
**Example 1: Multi-hop question requiring deep exploration (GitHub MCP)**
|
|
||||||
```xml
|
|
||||||
<qa_pair>
|
|
||||||
<question>Find the repository that was archived in Q3 2023 and had previously been the most forked project in the organization. What was the primary programming language used in that repository?</question>
|
|
||||||
<answer>Python</answer>
|
|
||||||
</qa_pair>
|
|
||||||
```
|
|
||||||
|
|
||||||
This question is good because:
|
|
||||||
- Requires multiple searches to find archived repositories
|
|
||||||
- Needs to identify which had the most forks before archival
|
|
||||||
- Requires examining repository details for the language
|
|
||||||
- Answer is a simple, verifiable value
|
|
||||||
- Based on historical (closed) data that won't change
|
|
||||||
|
|
||||||
**Example 2: Requires understanding context without keyword matching (Project Management MCP)**
|
|
||||||
```xml
|
|
||||||
<qa_pair>
|
|
||||||
<question>Locate the initiative focused on improving customer onboarding that was completed in late 2023. The project lead created a retrospective document after completion. What was the lead's role title at that time?</question>
|
|
||||||
<answer>Product Manager</answer>
|
|
||||||
</qa_pair>
|
|
||||||
```
|
|
||||||
|
|
||||||
This question is good because:
|
|
||||||
- Doesn't use specific project name ("initiative focused on improving customer onboarding")
|
|
||||||
- Requires finding completed projects from specific timeframe
|
|
||||||
- Needs to identify the project lead and their role
|
|
||||||
- Requires understanding context from retrospective documents
|
|
||||||
- Answer is human-readable and stable
|
|
||||||
- Based on completed work (won't change)
|
|
||||||
|
|
||||||
**Example 3: Complex aggregation requiring multiple steps (Issue Tracker MCP)**
|
|
||||||
```xml
|
|
||||||
<qa_pair>
|
|
||||||
<question>Among all bugs reported in January 2024 that were marked as critical priority, which assignee resolved the highest percentage of their assigned bugs within 48 hours? Provide the assignee's username.</question>
|
|
||||||
<answer>alex_eng</answer>
|
|
||||||
</qa_pair>
|
|
||||||
```
|
|
||||||
|
|
||||||
This question is good because:
|
|
||||||
- Requires filtering bugs by date, priority, and status
|
|
||||||
- Needs to group by assignee and calculate resolution rates
|
|
||||||
- Requires understanding timestamps to determine 48-hour windows
|
|
||||||
- Tests pagination (potentially many bugs to process)
|
|
||||||
- Answer is a single username
|
|
||||||
- Based on historical data from specific time period
|
|
||||||
|
|
||||||
**Example 4: Requires synthesis across multiple data types (CRM MCP)**
|
|
||||||
```xml
|
|
||||||
<qa_pair>
|
|
||||||
<question>Find the account that upgraded from the Starter to Enterprise plan in Q4 2023 and had the highest annual contract value. What industry does this account operate in?</question>
|
|
||||||
<answer>Healthcare</answer>
|
|
||||||
</qa_pair>
|
|
||||||
```
|
|
||||||
|
|
||||||
This question is good because:
|
|
||||||
- Requires understanding subscription tier changes
|
|
||||||
- Needs to identify upgrade events in specific timeframe
|
|
||||||
- Requires comparing contract values
|
|
||||||
- Must access account industry information
|
|
||||||
- Answer is simple and verifiable
|
|
||||||
- Based on completed historical transactions
|
|
||||||
|
|
||||||
### Poor Questions
|
|
||||||
|
|
||||||
**Example 1: Answer changes over time**
|
|
||||||
```xml
|
|
||||||
<qa_pair>
|
|
||||||
<question>How many open issues are currently assigned to the engineering team?</question>
|
|
||||||
<answer>47</answer>
|
|
||||||
</qa_pair>
|
|
||||||
```
|
|
||||||
|
|
||||||
This question is poor because:
|
|
||||||
- The answer will change as issues are created, closed, or reassigned
|
|
||||||
- Not based on stable/stationary data
|
|
||||||
- Relies on "current state" which is dynamic
|
|
||||||
|
|
||||||
**Example 2: Too easy with keyword search**
|
|
||||||
```xml
|
|
||||||
<qa_pair>
|
|
||||||
<question>Find the pull request with title "Add authentication feature" and tell me who created it.</question>
|
|
||||||
<answer>developer123</answer>
|
|
||||||
</qa_pair>
|
|
||||||
```
|
|
||||||
|
|
||||||
This question is poor because:
|
|
||||||
- Can be solved with a straightforward keyword search for exact title
|
|
||||||
- Doesn't require deep exploration or understanding
|
|
||||||
- No synthesis or analysis needed
|
|
||||||
|
|
||||||
**Example 3: Ambiguous answer format**
|
|
||||||
```xml
|
|
||||||
<qa_pair>
|
|
||||||
<question>List all the repositories that have Python as their primary language.</question>
|
|
||||||
<answer>repo1, repo2, repo3, data-pipeline, ml-tools</answer>
|
|
||||||
</qa_pair>
|
|
||||||
```
|
|
||||||
|
|
||||||
This question is poor because:
|
|
||||||
- Answer is a list that could be returned in any order
|
|
||||||
- Difficult to verify with direct string comparison
|
|
||||||
- LLM might format differently (JSON array, comma-separated, newline-separated)
|
|
||||||
- Better to ask for a specific aggregate (count) or superlative (most stars)
|
|
||||||
|
|
||||||
## Verification Process
|
|
||||||
|
|
||||||
After creating evaluations:
|
|
||||||
|
|
||||||
1. **Examine the XML file** to understand the schema
|
|
||||||
2. **Load each task instruction** and in parallel using the MCP server and tools, identify the correct answer by attempting to solve the task YOURSELF
|
|
||||||
3. **Flag any operations** that require WRITE or DESTRUCTIVE operations
|
|
||||||
4. **Accumulate all CORRECT answers** and replace any incorrect answers in the document
|
|
||||||
5. **Remove any `<qa_pair>`** that require WRITE or DESTRUCTIVE operations
|
|
||||||
|
|
||||||
Remember to parallelize solving tasks to avoid running out of context, then accumulate all answers and make changes to the file at the end.
|
|
||||||
|
|
||||||
## Tips for Creating Quality Evaluations
|
|
||||||
|
|
||||||
1. **Think Hard and Plan Ahead** before generating tasks
|
|
||||||
2. **Parallelize Where Opportunity Arises** to speed up the process and manage context
|
|
||||||
3. **Focus on Realistic Use Cases** that humans would actually want to accomplish
|
|
||||||
4. **Create Challenging Questions** that test the limits of the MCP server's capabilities
|
|
||||||
5. **Ensure Stability** by using historical data and closed concepts
|
|
||||||
6. **Verify Answers** by solving the questions yourself using the MCP server tools
|
|
||||||
7. **Iterate and Refine** based on what you learn during the process
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Running Evaluations
|
|
||||||
|
|
||||||
After creating your evaluation file, you can use the provided evaluation harness to test your MCP server.
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
1. **Install Dependencies**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r scripts/requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
Or install manually:
|
|
||||||
```bash
|
|
||||||
pip install anthropic mcp
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Set API Key**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export ANTHROPIC_API_KEY=your_api_key_here
|
|
||||||
```
|
|
||||||
|
|
||||||
## Evaluation File Format
|
|
||||||
|
|
||||||
Evaluation files use XML format with `<qa_pair>` elements:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<evaluation>
|
|
||||||
<qa_pair>
|
|
||||||
<question>Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name?</question>
|
|
||||||
<answer>Website Redesign</answer>
|
|
||||||
</qa_pair>
|
|
||||||
<qa_pair>
|
|
||||||
<question>Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username.</question>
|
|
||||||
<answer>sarah_dev</answer>
|
|
||||||
</qa_pair>
|
|
||||||
</evaluation>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running Evaluations
|
|
||||||
|
|
||||||
The evaluation script (`scripts/evaluation.py`) supports three transport types:
|
|
||||||
|
|
||||||
**Important:**
|
|
||||||
- **stdio transport**: The evaluation script automatically launches and manages the MCP server process for you. Do not run the server manually.
|
|
||||||
- **sse/http transports**: You must start the MCP server separately before running the evaluation. The script connects to the already-running server at the specified URL.
|
|
||||||
|
|
||||||
### 1. Local STDIO Server
|
|
||||||
|
|
||||||
For locally-run MCP servers (script launches the server automatically):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python scripts/evaluation.py \
|
|
||||||
-t stdio \
|
|
||||||
-c python \
|
|
||||||
-a my_mcp_server.py \
|
|
||||||
evaluation.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
With environment variables:
|
|
||||||
```bash
|
|
||||||
python scripts/evaluation.py \
|
|
||||||
-t stdio \
|
|
||||||
-c python \
|
|
||||||
-a my_mcp_server.py \
|
|
||||||
-e API_KEY=abc123 \
|
|
||||||
-e DEBUG=true \
|
|
||||||
evaluation.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Server-Sent Events (SSE)
|
|
||||||
|
|
||||||
For SSE-based MCP servers (you must start the server first):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python scripts/evaluation.py \
|
|
||||||
-t sse \
|
|
||||||
-u https://example.com/mcp \
|
|
||||||
-H "Authorization: Bearer token123" \
|
|
||||||
-H "X-Custom-Header: value" \
|
|
||||||
evaluation.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. HTTP (Streamable HTTP)
|
|
||||||
|
|
||||||
For HTTP-based MCP servers (you must start the server first):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python scripts/evaluation.py \
|
|
||||||
-t http \
|
|
||||||
-u https://example.com/mcp \
|
|
||||||
-H "Authorization: Bearer token123" \
|
|
||||||
evaluation.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
## Command-Line Options
|
|
||||||
|
|
||||||
```
|
|
||||||
usage: evaluation.py [-h] [-t {stdio,sse,http}] [-m MODEL] [-c COMMAND]
|
|
||||||
[-a ARGS [ARGS ...]] [-e ENV [ENV ...]] [-u URL]
|
|
||||||
[-H HEADERS [HEADERS ...]] [-o OUTPUT]
|
|
||||||
eval_file
|
|
||||||
|
|
||||||
positional arguments:
|
|
||||||
eval_file Path to evaluation XML file
|
|
||||||
|
|
||||||
optional arguments:
|
|
||||||
-h, --help Show help message
|
|
||||||
-t, --transport Transport type: stdio, sse, or http (default: stdio)
|
|
||||||
-m, --model Claude model to use (default: claude-3-7-sonnet-20250219)
|
|
||||||
-o, --output Output file for report (default: print to stdout)
|
|
||||||
|
|
||||||
stdio options:
|
|
||||||
-c, --command Command to run MCP server (e.g., python, node)
|
|
||||||
-a, --args Arguments for the command (e.g., server.py)
|
|
||||||
-e, --env Environment variables in KEY=VALUE format
|
|
||||||
|
|
||||||
sse/http options:
|
|
||||||
-u, --url MCP server URL
|
|
||||||
-H, --header HTTP headers in 'Key: Value' format
|
|
||||||
```
|
|
||||||
|
|
||||||
## Output
|
|
||||||
|
|
||||||
The evaluation script generates a detailed report including:
|
|
||||||
|
|
||||||
- **Summary Statistics**:
|
|
||||||
- Accuracy (correct/total)
|
|
||||||
- Average task duration
|
|
||||||
- Average tool calls per task
|
|
||||||
- Total tool calls
|
|
||||||
|
|
||||||
- **Per-Task Results**:
|
|
||||||
- Prompt and expected response
|
|
||||||
- Actual response from the agent
|
|
||||||
- Whether the answer was correct (✅/❌)
|
|
||||||
- Duration and tool call details
|
|
||||||
- Agent's summary of its approach
|
|
||||||
- Agent's feedback on the tools
|
|
||||||
|
|
||||||
### Save Report to File
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python scripts/evaluation.py \
|
|
||||||
-t stdio \
|
|
||||||
-c python \
|
|
||||||
-a my_server.py \
|
|
||||||
-o evaluation_report.md \
|
|
||||||
evaluation.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
## Complete Example Workflow
|
|
||||||
|
|
||||||
Here's a complete example of creating and running an evaluation:
|
|
||||||
|
|
||||||
1. **Create your evaluation file** (`my_evaluation.xml`):
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<evaluation>
|
|
||||||
<qa_pair>
|
|
||||||
<question>Find the user who created the most issues in January 2024. What is their username?</question>
|
|
||||||
<answer>alice_developer</answer>
|
|
||||||
</qa_pair>
|
|
||||||
<qa_pair>
|
|
||||||
<question>Among all pull requests merged in Q1 2024, which repository had the highest number? Provide the repository name.</question>
|
|
||||||
<answer>backend-api</answer>
|
|
||||||
</qa_pair>
|
|
||||||
<qa_pair>
|
|
||||||
<question>Find the project that was completed in December 2023 and had the longest duration from start to finish. How many days did it take?</question>
|
|
||||||
<answer>127</answer>
|
|
||||||
</qa_pair>
|
|
||||||
</evaluation>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Install dependencies**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r scripts/requirements.txt
|
|
||||||
export ANTHROPIC_API_KEY=your_api_key
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Run evaluation**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python scripts/evaluation.py \
|
|
||||||
-t stdio \
|
|
||||||
-c python \
|
|
||||||
-a github_mcp_server.py \
|
|
||||||
-e GITHUB_TOKEN=ghp_xxx \
|
|
||||||
-o github_eval_report.md \
|
|
||||||
my_evaluation.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Review the report** in `github_eval_report.md` to:
|
|
||||||
- See which questions passed/failed
|
|
||||||
- Read the agent's feedback on your tools
|
|
||||||
- Identify areas for improvement
|
|
||||||
- Iterate on your MCP server design
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Connection Errors
|
|
||||||
|
|
||||||
If you get connection errors:
|
|
||||||
- **STDIO**: Verify the command and arguments are correct
|
|
||||||
- **SSE/HTTP**: Check the URL is accessible and headers are correct
|
|
||||||
- Ensure any required API keys are set in environment variables or headers
|
|
||||||
|
|
||||||
### Low Accuracy
|
|
||||||
|
|
||||||
If many evaluations fail:
|
|
||||||
- Review the agent's feedback for each task
|
|
||||||
- Check if tool descriptions are clear and comprehensive
|
|
||||||
- Verify input parameters are well-documented
|
|
||||||
- Consider whether tools return too much or too little data
|
|
||||||
- Ensure error messages are actionable
|
|
||||||
|
|
||||||
### Timeout Issues
|
|
||||||
|
|
||||||
If tasks are timing out:
|
|
||||||
- Use a more capable model (e.g., `claude-3-7-sonnet-20250219`)
|
|
||||||
- Check if tools are returning too much data
|
|
||||||
- Verify pagination is working correctly
|
|
||||||
- Consider simplifying complex questions
|
|
||||||
@@ -58,7 +58,7 @@ public class EaiEimsSender implements EimsSender {
|
|||||||
log.warn(" 로컬 환경이거나 Ka<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
|
log.warn(" 로컬 환경이거나 Ka<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
|
||||||
"<EaiMessage>\n" +
|
"<EaiMessage>\n" +
|
||||||
" <Header>\n" +
|
" <Header>\n" +
|
||||||
" <ChannelId>MCP_WAS_ASYNC</ChannelId>\n" +
|
" <ChannelId>MCP_GATEWAY_ASYNC</ChannelId>\n" +
|
||||||
" <InterfaceId>EAI_BATCH_JOB</InterfaceId>\n" +
|
" <InterfaceId>EAI_BATCH_JOB</InterfaceId>\n" +
|
||||||
" <Timestamp>1782705901850</Timestamp>\n" +
|
" <Timestamp>1782705901850</Timestamp>\n" +
|
||||||
" <TransferType>ASYNC</TransferType>\n" +
|
" <TransferType>ASYNC</TransferType>\n" +
|
||||||
@@ -91,7 +91,7 @@ public class EaiEimsSender implements EimsSender {
|
|||||||
return String.format(
|
return String.format(
|
||||||
"<EaiMessage>" +
|
"<EaiMessage>" +
|
||||||
"<Header>" +
|
"<Header>" +
|
||||||
"<ChannelId>MCP_WAS_ASYNC</ChannelId>" +
|
"<ChannelId>MCP_GATEWAY_ASYNC</ChannelId>" +
|
||||||
"<InterfaceId>%s</InterfaceId>" +
|
"<InterfaceId>%s</InterfaceId>" +
|
||||||
"<Timestamp>%d</Timestamp>" +
|
"<Timestamp>%d</Timestamp>" +
|
||||||
"<TransferType>ASYNC</TransferType>" +
|
"<TransferType>ASYNC</TransferType>" +
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package io.shinhanlife.dap.lib.annotation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.annotation
|
|
||||||
* @className McpAnyOf
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import java.lang.annotation.ElementType;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.lang.annotation.Target;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MCP 스키마 생성 시 anyOf (해당 필드들 중 최소 1개 이상 필수) 제약을 부여합니다.
|
|
||||||
*/
|
|
||||||
@Target({ElementType.TYPE})
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
public @interface McpAnyOf {
|
|
||||||
/**
|
|
||||||
* anyOf 제약에 포함될 필드명 목록
|
|
||||||
* 예: @McpAnyOf({"claimNo", "contractNo"})
|
|
||||||
*/
|
|
||||||
String[] value();
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package io.shinhanlife.dap.lib.annotation;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.annotation
|
|
||||||
* @className McpFunction
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
import java.lang.annotation.*;
|
|
||||||
|
|
||||||
@Target(ElementType.METHOD)
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Documented
|
|
||||||
public @interface McpFunction {
|
|
||||||
String displayName(); // 사람이 읽는 라벨 (예: "고객 조회 툴")
|
|
||||||
String name(); // MCP 서브툴 명칭 (예: "customer_search")
|
|
||||||
String description();
|
|
||||||
String prompt() default "";
|
|
||||||
String mappingId() default "";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tool 입력 JSON Schema를 인라인으로 지정한다. 지정하지 않으면 요청 DTO에서 자동 생성한다.
|
|
||||||
*/
|
|
||||||
String inputSchema() default "{}";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 복합 조건(anyOf 등)이 필요한 Tool의 입력 JSON Schema 클래스패스 경로다.
|
|
||||||
* inputSchemaResource가 지정되면 inputSchema 및 DTO 자동 생성보다 우선한다.
|
|
||||||
*/
|
|
||||||
// 추가: Redis 자동 등록 및 Heartbeat 대상 여부 제어
|
|
||||||
String inputSchemaResource() default "";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tool response JSON Schema. When unset, output validation is skipped.
|
|
||||||
*/
|
|
||||||
String outputSchema() default "{}";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Classpath resource for a complex Tool response JSON Schema.
|
|
||||||
* This value has priority over outputSchema.
|
|
||||||
*/
|
|
||||||
String outputSchemaResource() default "";
|
|
||||||
boolean register() default false;
|
|
||||||
|
|
||||||
// 추가: 툴 목록 노출 여부 제어 (false 시 라우팅은 되나 목록에서 숨김)
|
|
||||||
boolean visible() default true;
|
|
||||||
|
|
||||||
// 추가: HITL 승인 체계 지원 (실행 전 사용자 승인 필요 여부)
|
|
||||||
boolean requiresApproval() default false;
|
|
||||||
|
|
||||||
boolean readOnlyHint() default false;
|
|
||||||
boolean destructiveHint() default false;
|
|
||||||
boolean idempotentHint() default false;
|
|
||||||
boolean openWorldHint() default false;
|
|
||||||
|
|
||||||
/** Version exposed as _meta.version in the Tool Manifest. */
|
|
||||||
String version() default "1.0.0";
|
|
||||||
|
|
||||||
/** Maximum execution time exposed as _meta.timeoutMillis in the Tool Manifest. */
|
|
||||||
long timeoutMillis() default 300000L;
|
|
||||||
|
|
||||||
/** Whether the Tool is available for MCP exposure. */
|
|
||||||
boolean enabled() default true;
|
|
||||||
|
|
||||||
// 추가: 툴 별 기본 Timeout 설정 (기본 300초 = 300000ms)
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package io.shinhanlife.dap.lib.annotation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.annotation
|
|
||||||
* @className McpOutputSchema
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import java.lang.annotation.Documented;
|
|
||||||
import java.lang.annotation.ElementType;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.lang.annotation.Target;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks a Tool response DTO for automatic output JSON Schema generation.
|
|
||||||
* Field constraints are declared with {@link McpValidation}.
|
|
||||||
*/
|
|
||||||
@Target(ElementType.TYPE)
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Documented
|
|
||||||
public @interface McpOutputSchema {
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package io.shinhanlife.dap.lib.annotation;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.annotation
|
|
||||||
* @className McpParameter
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
import java.lang.annotation.*;
|
|
||||||
|
|
||||||
@Target(ElementType.FIELD)
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Documented
|
|
||||||
public @interface McpParameter {
|
|
||||||
String description();
|
|
||||||
boolean required() default false;
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package io.shinhanlife.dap.lib.annotation;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.annotation
|
|
||||||
* @className McpTool
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
import org.springframework.core.annotation.AliasFor;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.lang.annotation.*;
|
|
||||||
|
|
||||||
@Target(ElementType.TYPE)
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Documented
|
|
||||||
@Component
|
|
||||||
public @interface McpTool {
|
|
||||||
@AliasFor(annotation = Component.class)
|
|
||||||
String value() default "";
|
|
||||||
|
|
||||||
String categoryKey() default "common";
|
|
||||||
String routingType() default "HTTP";
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package io.shinhanlife.dap.lib.annotation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.annotation
|
|
||||||
* @className McpValidation
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import java.lang.annotation.Documented;
|
|
||||||
import java.lang.annotation.ElementType;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.lang.annotation.Target;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.annotation
|
|
||||||
* @className McpValidation
|
|
||||||
* @description Declares JSON Schema validation constraints for MCP tool input fields
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.07.27
|
|
||||||
* <pre>
|
|
||||||
* ---------- revision history ----------
|
|
||||||
* date author description
|
|
||||||
* ---------- --------- ---------------------------
|
|
||||||
* 2026.07.27 0986406 initial creation
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
@Target(ElementType.FIELD)
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Documented
|
|
||||||
public @interface McpValidation {
|
|
||||||
boolean required() default false;
|
|
||||||
String pattern() default "";
|
|
||||||
long minimum() default Long.MIN_VALUE;
|
|
||||||
long maximum() default Long.MAX_VALUE;
|
|
||||||
int minLength() default -1;
|
|
||||||
int maxLength() default -1;
|
|
||||||
String[] allowedValues() default {};
|
|
||||||
String format() default "";
|
|
||||||
boolean nullable() default false; String defaultValue() default "";
|
|
||||||
String[] examples() default {};
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package io.shinhanlife.dap.lib.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring AI @Tool 어노테이션을 보완하여 MCP 시스템 메타데이터를 추가 제공하는 힌트 어노테이션
|
||||||
|
* @package io.shinhanlife.dap.lib.annotation
|
||||||
|
* @className ToolHint
|
||||||
|
* @description 비즈니스 로직(Tool)과 시스템 제어 메타데이터 분리
|
||||||
|
* @author 0986406
|
||||||
|
* @create 2026.09.01
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.09.01 0986406 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface ToolHint {
|
||||||
|
boolean register() default false;
|
||||||
|
boolean requiresApproval() default false;
|
||||||
|
String categoryKey() default "com";
|
||||||
|
String mappingId() default "";
|
||||||
|
String inputSchemaResource() default "";
|
||||||
|
String outputSchemaResource() default "";
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@ package io.shinhanlife.dap.lib.aop;
|
|||||||
*
|
*
|
||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
|
||||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -24,6 +23,7 @@ import org.aspectj.lang.ProceedingJoinPoint;
|
|||||||
import org.aspectj.lang.annotation.Around;
|
import org.aspectj.lang.annotation.Around;
|
||||||
import org.aspectj.lang.annotation.Aspect;
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
import org.aspectj.lang.reflect.MethodSignature;
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpTool;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.StopWatch;
|
import org.springframework.util.StopWatch;
|
||||||
|
|
||||||
@@ -35,12 +35,12 @@ public class ToolSlaMonitoringAspect {
|
|||||||
|
|
||||||
private final McpProperties mcpProperties;
|
private final McpProperties mcpProperties;
|
||||||
|
|
||||||
// @McpFunction 어노테이션이 붙은 모든 비즈니스 툴 메서드 실행을 가로챕니다.
|
// @McpTool 어노테이션이 붙은 모든 비즈니스 툴 메서드 실행을 가로챕니다.
|
||||||
@Around("@annotation(McpFunction)")
|
@Around("@annotation(org.springframework.ai.mcp.annotation.McpTool)")
|
||||||
public Object monitorToolSla(ProceedingJoinPoint joinPoint) throws Throwable {
|
public Object monitorToolSla(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||||
Method method = signature.getMethod();
|
Method method = signature.getMethod();
|
||||||
McpFunction functionAnnotation = method.getAnnotation(McpFunction.class);
|
McpTool functionAnnotation = method.getAnnotation(McpTool.class);
|
||||||
|
|
||||||
// 네임스페이스 자동 주입 로직을 반영하여 최종 툴 이름을 산출합니다.
|
// 네임스페이스 자동 주입 로직을 반영하여 최종 툴 이름을 산출합니다.
|
||||||
String baseName = functionAnnotation.name();
|
String baseName = functionAnnotation.name();
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.dap.lib.config;
|
package io.shinhanlife.dap.lib.config;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.config
|
|
||||||
* @className ToolSchemaConfiguration
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package io.shinhanlife.dap.lib.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcg.dto
|
||||||
|
* @className OperationType
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 0986406
|
||||||
|
* @create 2026.09.01
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.09.01 0986406 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public enum OperationType {
|
||||||
|
READ,
|
||||||
|
WRITE
|
||||||
|
}
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.dap.lib.manifest;
|
package io.shinhanlife.dap.lib.manifest;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.manifest
|
|
||||||
* @className ToolManifestAnnotations
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/** Behaviour hints exposed by the Tool Service manifest. */
|
/** Behaviour hints exposed by the Tool Service manifest. */
|
||||||
public record ToolManifestAnnotations(
|
public record ToolManifestAnnotations(
|
||||||
String title,
|
String title,
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.dap.lib.manifest;
|
package io.shinhanlife.dap.lib.manifest;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.manifest
|
|
||||||
* @className ToolManifestItem
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.dap.lib.manifest;
|
package io.shinhanlife.dap.lib.manifest;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.manifest
|
|
||||||
* @className ToolManifestMeta
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/** Operational metadata exposed by the Tool Service manifest. */
|
/** Operational metadata exposed by the Tool Service manifest. */
|
||||||
public record ToolManifestMeta(String version, long timeoutMillis, boolean enabled) {
|
public record ToolManifestMeta(String version, long timeoutMillis, boolean enabled) {
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.dap.lib.manifest;
|
package io.shinhanlife.dap.lib.manifest;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.manifest
|
|
||||||
* @className ToolManifestResponse
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** Top-level response for GET /tool-manifest. */
|
/** Top-level response for GET /tool-manifest. */
|
||||||
|
|||||||
@@ -1,25 +1,9 @@
|
|||||||
package io.shinhanlife.dap.lib.manifest;
|
package io.shinhanlife.dap.lib.manifest;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.manifest
|
|
||||||
* @className ToolManifestService
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||||
import io.shinhanlife.dap.lib.mcp.LocalToolScanner;
|
import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -46,9 +30,9 @@ public class ToolManifestService {
|
|||||||
private String lastRevision;
|
private String lastRevision;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ToolManifestService(LocalToolScanner localToolScanner, ObjectMapper objectMapper,
|
public ToolManifestService(ToolRegistryHeartbeatSender heartbeatSender, ObjectMapper objectMapper,
|
||||||
McpProperties properties) {
|
McpProperties properties) {
|
||||||
this(localToolScanner::getAllScannedTools, objectMapper, properties);
|
this(heartbeatSender::getAllScannedTools, objectMapper, properties);
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolManifestService(Supplier<List<ToolMetadata>> toolSupplier, ObjectMapper objectMapper,
|
ToolManifestService(Supplier<List<ToolMetadata>> toolSupplier, ObjectMapper objectMapper,
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
package io.shinhanlife.dap.lib.mcp;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.mcc.service
|
|
||||||
* @className LocalToolScanner
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpTool;
|
|
||||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
|
||||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
|
||||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.UUID;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.aop.support.AopUtils;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.context.ApplicationContext;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.core.annotation.AnnotationUtils;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.util.ClassUtils;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
@Configuration
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class LocalToolScanner {
|
|
||||||
|
|
||||||
private final ApplicationContext applicationContext;
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
private final McpProperties mcpProperties;
|
|
||||||
private final ToolSchemaResolver toolSchemaResolver;
|
|
||||||
|
|
||||||
@Value("${axhub.tool.url:http://localhost:8080}")
|
|
||||||
private String podUrl;
|
|
||||||
|
|
||||||
private List<ToolMetadata> registeredTools = new ArrayList<>();
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private List<ToolMetadata> allScannedTools = new ArrayList<>();
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void init() {
|
|
||||||
log.info(" [LocalToolScanner] 초기화 시작. Pod URL: {}", podUrl);
|
|
||||||
scanAndBuildMetadata();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scanAndBuildMetadata() {
|
|
||||||
Map<String, Object> allBeans = applicationContext.getBeansOfType(Object.class);
|
|
||||||
for (Object bean : allBeans.values()) {
|
|
||||||
Class<?> targetClass = AopUtils.getTargetClass(bean);
|
|
||||||
|
|
||||||
// 클래스 또는 프록시(인터페이스)에서 @McpTool 스캔
|
|
||||||
McpTool toolAnnotation = AnnotationUtils.findAnnotation(targetClass, McpTool.class);
|
|
||||||
if (toolAnnotation == null) {
|
|
||||||
toolAnnotation = AnnotationUtils.findAnnotation(bean.getClass(), McpTool.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Method method : targetClass.getDeclaredMethods()) {
|
|
||||||
// 메서드, 수퍼클래스, 인터페이스를 모두 뒤져서 @McpFunction 스캔
|
|
||||||
McpFunction functionAnnotation = AnnotationUtils.findAnnotation(method, McpFunction.class);
|
|
||||||
|
|
||||||
if (functionAnnotation != null && toolAnnotation != null) {
|
|
||||||
String baseName = functionAnnotation.displayName();
|
|
||||||
String rawSubToolName = functionAnnotation.name();
|
|
||||||
String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
|
||||||
? mcpProperties.getNamespace() + "_" + rawSubToolName
|
|
||||||
: rawSubToolName;
|
|
||||||
|
|
||||||
boolean isRegister = functionAnnotation.register();
|
|
||||||
if (!isRegister) {
|
|
||||||
log.info(" [LocalToolScanner] '{}' 툴은 어노테이션 설정에 의해 제외되었습니다. (최종 이름: {})", baseName, subToolName);
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolMetadata meta = new ToolMetadata();
|
|
||||||
meta.setUid(UUID.nameUUIDFromBytes(subToolName.getBytes()).toString());
|
|
||||||
meta.setDisplayName(baseName);
|
|
||||||
meta.setName(subToolName);
|
|
||||||
meta.setSemver(functionAnnotation.version());
|
|
||||||
meta.setTimeoutMillis(functionAnnotation.timeoutMillis());
|
|
||||||
meta.setEnabled(functionAnnotation.enabled());
|
|
||||||
meta.setDescription(functionAnnotation.description());
|
|
||||||
meta.setCategoryKey(toolAnnotation.categoryKey());
|
|
||||||
meta.setIntegrationType(toolAnnotation.routingType());
|
|
||||||
meta.setMciServiceId(functionAnnotation.mappingId());
|
|
||||||
meta.setPodUrl(podUrl);
|
|
||||||
meta.setEndpoint(podUrl.replaceAll("/+$", "") + "/mcp/" + subToolName);
|
|
||||||
|
|
||||||
boolean isVisible = functionAnnotation.visible();
|
|
||||||
meta.setVisible(isVisible);
|
|
||||||
meta.setIsRegistered(isRegister);
|
|
||||||
meta.setRequiresApproval(functionAnnotation.requiresApproval());
|
|
||||||
meta.setReadOnlyHint(functionAnnotation.readOnlyHint());
|
|
||||||
meta.setDestructiveHint(functionAnnotation.destructiveHint());
|
|
||||||
meta.setIdempotentHint(functionAnnotation.idempotentHint());
|
|
||||||
meta.setOpenWorldHint(functionAnnotation.openWorldHint());
|
|
||||||
|
|
||||||
Map<String, String> prompts = new HashMap<>();
|
|
||||||
String promptText = functionAnnotation.prompt();
|
|
||||||
prompts.put(subToolName, promptText);
|
|
||||||
meta.setActionPrompts(prompts);
|
|
||||||
|
|
||||||
if (method.getParameterCount() > 0) {
|
|
||||||
try {
|
|
||||||
Class<?> paramType = method.getParameterTypes()[0];
|
|
||||||
Map<String, Object> finalSchema = toolSchemaResolver.resolve(functionAnnotation, paramType);
|
|
||||||
meta.setParametersSchema(finalSchema);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to generate schema for {}", subToolName, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRegister) {
|
|
||||||
registeredTools.add(meta);
|
|
||||||
}
|
|
||||||
allScannedTools.add(meta);
|
|
||||||
log.info(" [LocalToolScanner] 도구 메타데이터 생성: {} (isRegistered: {})", meta.getUid(), isRegister);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.dap.lib.mcp;
|
package io.shinhanlife.dap.lib.mcp;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.mcp
|
|
||||||
* @className McpRequestHeaderContext
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/** Holds optional MCP headers for the lifetime of one HTTP request thread. */
|
/** Holds optional MCP headers for the lifetime of one HTTP request thread. */
|
||||||
public final class McpRequestHeaderContext {
|
public final class McpRequestHeaderContext {
|
||||||
private static final ThreadLocal<McpRequestHeaders> CURRENT_HEADERS = new ThreadLocal<>();
|
private static final ThreadLocal<McpRequestHeaders> CURRENT_HEADERS = new ThreadLocal<>();
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.dap.lib.mcp;
|
package io.shinhanlife.dap.lib.mcp;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.mcp
|
|
||||||
* @className McpRequestHeaderFilter
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.dap.lib.mcp;
|
package io.shinhanlife.dap.lib.mcp;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.mcp
|
|
||||||
* @className McpRequestHeaders
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/** Optional request headers propagated from an MCP HTTP request to a Tool invocation. */
|
/** Optional request headers propagated from an MCP HTTP request to a Tool invocation. */
|
||||||
public record McpRequestHeaders(
|
public record McpRequestHeaders(
|
||||||
String headerRequestId,
|
String headerRequestId,
|
||||||
|
|||||||
@@ -1,31 +1,25 @@
|
|||||||
package io.shinhanlife.dap.lib.mcp;
|
package io.shinhanlife.dap.lib.mcp;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.mcp
|
|
||||||
* @className ToolMcpServerConfiguration
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
|
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
|
||||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Primary;
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/** Exposes every Tool Pod through the MCP Streamable HTTP transport. */
|
/** Exposes every Tool Pod through the MCP Streamable HTTP transport. */
|
||||||
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
public class ToolMcpServerConfiguration {
|
public class ToolMcpServerConfiguration {
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
log.warn("=================================================");
|
||||||
|
log.warn("ToolMcpServerConfiguration IS LOADED BY SPRING!!!");
|
||||||
|
log.warn("=================================================");
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Primary
|
@Primary
|
||||||
public HttpServletStreamableServerTransportProvider toolMcpTransportProvider() {
|
public HttpServletStreamableServerTransportProvider toolMcpTransportProvider() {
|
||||||
@@ -37,6 +31,8 @@ public class ToolMcpServerConfiguration {
|
|||||||
@Bean
|
@Bean
|
||||||
public ServletRegistrationBean<HttpServletStreamableServerTransportProvider> toolMcpServlet(
|
public ServletRegistrationBean<HttpServletStreamableServerTransportProvider> toolMcpServlet(
|
||||||
HttpServletStreamableServerTransportProvider transportProvider) {
|
HttpServletStreamableServerTransportProvider transportProvider) {
|
||||||
return new ServletRegistrationBean<>(transportProvider, "/mcp");
|
// "/mcp/*"로 매핑하면 BusinessToolController의 "/mcp/api/v1/tools/local" 까지 가로채게 되므로,
|
||||||
|
// 정확히 MCP 통신에 사용되는 "/mcp" 와 "/mcp/message" 두 개만 매핑합니다.
|
||||||
|
return new ServletRegistrationBean<>(transportProvider, "/mcp", "/mcp/message");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,14 @@
|
|||||||
package io.shinhanlife.dap.lib.mcp;
|
package io.shinhanlife.dap.lib.mcp;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.mcp
|
|
||||||
* @className ToolPodMcpToolSynchronizer
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.modelcontextprotocol.server.McpServerFeatures;
|
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||||
import io.modelcontextprotocol.server.McpSyncServer;
|
import io.modelcontextprotocol.server.McpSyncServer;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||||
import io.shinhanlife.dap.lib.presentation.BusinessToolController;
|
import io.shinhanlife.dap.mcc.presentation.BusinessToolController;
|
||||||
import io.shinhanlife.dap.lib.mcp.LocalToolScanner;
|
import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender;
|
||||||
|
import io.shinhanlife.dap.lib.mcp.McpRequestHeaderContext;
|
||||||
|
import io.shinhanlife.dap.lib.mcp.McpRequestHeaders;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
@@ -30,25 +16,28 @@ import org.springframework.context.event.EventListener;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
|
||||||
/** Registers the Tool Pod's existing annotated tools with its MCP SDK server. */
|
/** Registers the Tool Pod's existing annotated tools with its MCP SDK server. */
|
||||||
@Component
|
@Component
|
||||||
|
@ConditionalOnBean(BusinessToolController.class)
|
||||||
public class ToolPodMcpToolSynchronizer {
|
public class ToolPodMcpToolSynchronizer {
|
||||||
private final McpSyncServer mcpServer;
|
private final McpSyncServer mcpServer;
|
||||||
private final LocalToolScanner localToolScanner;
|
private final ToolRegistryHeartbeatSender heartbeatSender;
|
||||||
private final BusinessToolController businessToolController;
|
private final BusinessToolController businessToolController;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public ToolPodMcpToolSynchronizer(McpSyncServer mcpServer, LocalToolScanner localToolScanner,
|
public ToolPodMcpToolSynchronizer(McpSyncServer mcpServer, ToolRegistryHeartbeatSender heartbeatSender,
|
||||||
BusinessToolController businessToolController, ObjectMapper objectMapper) {
|
BusinessToolController businessToolController, ObjectMapper objectMapper) {
|
||||||
this.mcpServer = mcpServer;
|
this.mcpServer = mcpServer;
|
||||||
this.localToolScanner = localToolScanner;
|
this.heartbeatSender = heartbeatSender;
|
||||||
this.businessToolController = businessToolController;
|
this.businessToolController = businessToolController;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
public void registerLocalTools() {
|
public void registerLocalTools() {
|
||||||
localToolScanner.getAllScannedTools().stream()
|
heartbeatSender.getAllScannedTools().stream()
|
||||||
.filter(tool -> Boolean.TRUE.equals(tool.getVisible()))
|
.filter(tool -> Boolean.TRUE.equals(tool.getVisible()))
|
||||||
.forEach(tool -> mcpServer.addTool(specification(tool)));
|
.forEach(tool -> mcpServer.addTool(specification(tool)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package io.shinhanlife.dap.lib.mcp;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.service
|
||||||
|
* @className ToolRegistryHeartbeatSender
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 0986406
|
||||||
|
* @create 2026.09.01
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.09.01 0986406 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpTool;
|
||||||
|
import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||||
|
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||||
|
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||||
|
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.aop.support.AopUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import io.shinhanlife.dap.mcc.presentation.BusinessToolController;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@Configuration
|
||||||
|
@EnableScheduling
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConditionalOnBean(BusinessToolController.class)
|
||||||
|
public class ToolRegistryHeartbeatSender {
|
||||||
|
|
||||||
|
private final ApplicationContext applicationContext;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final McpProperties mcpProperties;
|
||||||
|
private final RestClient restClient = RestClient.create();
|
||||||
|
private final ToolSchemaResolver toolSchemaResolver;
|
||||||
|
|
||||||
|
@Value("${axhub.gateway.url:http://localhost:8081}")
|
||||||
|
private String gatewayUrl;
|
||||||
|
|
||||||
|
@Value("${axhub.tool.url:http://localhost:8080}")
|
||||||
|
private String podUrl;
|
||||||
|
|
||||||
|
private List<ToolMetadata> registeredTools = new ArrayList<>();
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private List<ToolMetadata> allScannedTools = new ArrayList<>();
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
log.info(" [HeartbeatSender] 초기화 시작. Gateway URL: {}, Pod URL: {}", gatewayUrl, podUrl);
|
||||||
|
scanAndBuildMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scanAndBuildMetadata() {
|
||||||
|
Map<String, Object> allBeans = applicationContext.getBeansOfType(Object.class);
|
||||||
|
for (Object bean : allBeans.values()) {
|
||||||
|
Class<?> targetClass = AopUtils.getTargetClass(bean);
|
||||||
|
|
||||||
|
for (Method method : targetClass.getDeclaredMethods()) {
|
||||||
|
McpTool functionAnnotation = AnnotationUtils.findAnnotation(method, McpTool.class);
|
||||||
|
ToolHint hintAnnotation = AnnotationUtils.findAnnotation(method, ToolHint.class);
|
||||||
|
|
||||||
|
if (functionAnnotation != null) {
|
||||||
|
String baseName = functionAnnotation.name();
|
||||||
|
String rawSubToolName = functionAnnotation.name();
|
||||||
|
String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
||||||
|
? mcpProperties.getNamespace() + "_" + rawSubToolName
|
||||||
|
: rawSubToolName;
|
||||||
|
|
||||||
|
boolean isRegister = hintAnnotation != null && hintAnnotation.register();
|
||||||
|
if (!isRegister) {
|
||||||
|
log.info(" [HeartbeatSender] '{}' 툴은 어노테이션 설정에 의해 외부 등록(Redis) 대상에서 제외되었습니다. (최종 이름: {})", baseName, subToolName);
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolMetadata meta = new ToolMetadata();
|
||||||
|
meta.setUid(UUID.nameUUIDFromBytes(subToolName.getBytes()).toString());
|
||||||
|
String displayName = functionAnnotation.title().isEmpty() ? functionAnnotation.name() : functionAnnotation.title();
|
||||||
|
meta.setDisplayName(displayName);
|
||||||
|
meta.setName(subToolName);
|
||||||
|
meta.setSemver("1.0.0");
|
||||||
|
meta.setTimeoutMillis(5000L);
|
||||||
|
meta.setEnabled(true);
|
||||||
|
meta.setDescription(functionAnnotation.description());
|
||||||
|
meta.setCategoryKey(hintAnnotation == null || hintAnnotation.categoryKey().isBlank()
|
||||||
|
? "common" : hintAnnotation.categoryKey());
|
||||||
|
meta.setIntegrationType("REST");
|
||||||
|
meta.setMciServiceId(hintAnnotation != null ? hintAnnotation.mappingId() : "");
|
||||||
|
meta.setPodUrl(podUrl);
|
||||||
|
meta.setEndpoint(podUrl.replaceAll("/+$", "") + "/mcp/" + subToolName);
|
||||||
|
|
||||||
|
meta.setVisible(true);
|
||||||
|
meta.setIsRegistered(isRegister);
|
||||||
|
meta.setRequiresApproval(hintAnnotation != null && hintAnnotation.requiresApproval());
|
||||||
|
|
||||||
|
// extract standard hints from @McpTool.annotations()
|
||||||
|
McpTool.McpAnnotations ann = functionAnnotation.annotations();
|
||||||
|
if (ann != null) {
|
||||||
|
meta.setReadOnlyHint(ann.readOnlyHint());
|
||||||
|
meta.setDestructiveHint(ann.destructiveHint());
|
||||||
|
meta.setIdempotentHint(ann.idempotentHint());
|
||||||
|
meta.setOpenWorldHint(ann.openWorldHint());
|
||||||
|
} else {
|
||||||
|
meta.setReadOnlyHint(false);
|
||||||
|
meta.setDestructiveHint(true);
|
||||||
|
meta.setIdempotentHint(false);
|
||||||
|
meta.setOpenWorldHint(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> prompts = new HashMap<>();
|
||||||
|
meta.setActionPrompts(prompts);
|
||||||
|
|
||||||
|
if (method.getParameterCount() > 0) {
|
||||||
|
try {
|
||||||
|
Class<?> paramType = method.getParameterTypes()[0];
|
||||||
|
// TODO: ToolSchemaResolver may need to be updated to take McpTool instead of McpFunction
|
||||||
|
Map<String, Object> finalSchema = toolSchemaResolver.resolve(functionAnnotation, hintAnnotation, paramType);
|
||||||
|
meta.setParametersSchema(finalSchema);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to generate schema for {}", subToolName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRegister) {
|
||||||
|
registeredTools.add(meta);
|
||||||
|
}
|
||||||
|
allScannedTools.add(meta);
|
||||||
|
log.info(" [HeartbeatSender] 도구 메타데이터 생성: {} (isRegistered: {})", meta.getUid(), isRegister);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(fixedRate = 30000)
|
||||||
|
public void sendHeartbeats() {
|
||||||
|
if (registeredTools.isEmpty()) return;
|
||||||
|
|
||||||
|
for (ToolMetadata tool : registeredTools) {
|
||||||
|
try {
|
||||||
|
ResponseEntity<String> response = restClient.post()
|
||||||
|
.uri(gatewayUrl + "/mcp/api/v1/registry/heartbeat")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
|
||||||
|
.body(tool.getUid())
|
||||||
|
.retrieve()
|
||||||
|
.toEntity(String.class);
|
||||||
|
|
||||||
|
if (response.getStatusCode().is2xxSuccessful()) {
|
||||||
|
log.info(" [HeartbeatSender] 하트비트 전송 성공: {}", tool.getUid());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(" [HeartbeatSender] 하트비트 전송 실패 ({}): {}. 재등록을 시도합니다.", tool.getUid(), e.getMessage());
|
||||||
|
registerTool(tool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerTool(ToolMetadata tool) {
|
||||||
|
try {
|
||||||
|
restClient.post()
|
||||||
|
.uri(gatewayUrl + "/mcp/api/v1/registry/register")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
|
||||||
|
.body(tool)
|
||||||
|
.retrieve()
|
||||||
|
.toBodilessEntity();
|
||||||
|
log.info(" [HeartbeatSender] 툴 재등록 성공: {}", tool.getUid());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error(" [HeartbeatSender] 툴 등록 실패: {}", ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,11 +29,11 @@ public class SwaggerConfig {
|
|||||||
public OpenAPI customOpenAPI() {
|
public OpenAPI customOpenAPI() {
|
||||||
return new OpenAPI()
|
return new OpenAPI()
|
||||||
.info(new Info()
|
.info(new Info()
|
||||||
.title("Shinhan MCP WAS API 명세서")
|
.title("Shinhan MCP Gateway API 명세서")
|
||||||
.version("v1.0")
|
.version("v1.0")
|
||||||
.description("AI Agent와 신한라이프 내부망(EIMS/EAI)을 연결하는 Adapter WAS API 문서입니다."))
|
.description("AI Agent와 신한라이프 내부망(EIMS/EAI)을 연결하는 Adapter Gateway API 문서입니다."))
|
||||||
.addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8080").description("Adapter Pod (8080)"))
|
.addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8080").description("Adapter Pod (8080)"))
|
||||||
.addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8081").description("WAS Pod"))
|
.addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8081").description("Gateway Pod (8081)"))
|
||||||
// 전역적으로 X-API-KEY 보안 설정을 Swagger UI에 추가합니다.
|
// 전역적으로 X-API-KEY 보안 설정을 Swagger UI에 추가합니다.
|
||||||
.addSecurityItem(new SecurityRequirement().addList("X-API-KEY"))
|
.addSecurityItem(new SecurityRequirement().addList("X-API-KEY"))
|
||||||
.components(new Components()
|
.components(new Components()
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public class WebConfig implements WebMvcConfigurer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addCorsMappings(CorsRegistry registry) {
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
// Swagger UI(8080)에서 WAS로 API 호출 시 발생하는 CORS 에러 해결
|
// Swagger UI(8080)에서 Gateway(8081)로 API 호출 시 발생하는 CORS 에러 해결
|
||||||
registry.addMapping("/**")
|
registry.addMapping("/**")
|
||||||
.allowedOriginPatterns("*")
|
.allowedOriginPatterns("*")
|
||||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||||
|
|||||||
@@ -29,25 +29,25 @@ public class GlobalExceptionHandler {
|
|||||||
|
|
||||||
@ExceptionHandler(NoResourceFoundException.class)
|
@ExceptionHandler(NoResourceFoundException.class)
|
||||||
public ResponseEntity<Void> handleNoResourceFound(NoResourceFoundException e) {
|
public ResponseEntity<Void> handleNoResourceFound(NoResourceFoundException e) {
|
||||||
log.warn(" [WAS Not Found] 요청하신 리소스를 찾을 수 없습니다: {}", e.getResourcePath());
|
log.warn(" [Gateway Not Found] 요청하신 리소스를 찾을 수 없습니다: {}", e.getResourcePath());
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(IllegalArgumentException.class)
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
public ResponseEntity<JsonRpcResponse> handleIllegalArgument(IllegalArgumentException e) {
|
public ResponseEntity<JsonRpcResponse> handleIllegalArgument(IllegalArgumentException e) {
|
||||||
log.warn(" [WAS Bad Request] 잘못된 요청: {}", e.getMessage());
|
log.warn(" [Gateway Bad Request] 잘못된 요청: {}", e.getMessage());
|
||||||
return buildErrorResponse(-32602, "Invalid params: " + e.getMessage());
|
return buildErrorResponse(-32602, "Invalid params: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(RuntimeException.class)
|
@ExceptionHandler(RuntimeException.class)
|
||||||
public ResponseEntity<JsonRpcResponse> handleRuntime(RuntimeException e) {
|
public ResponseEntity<JsonRpcResponse> handleRuntime(RuntimeException e) {
|
||||||
log.error(" [WAS Internal Error] 시스템 장애: {}", e.getMessage(), e);
|
log.error(" [Gateway Internal Error] 시스템 장애: {}", e.getMessage(), e);
|
||||||
return buildErrorResponse(-32603, "Internal error: " + e.getMessage());
|
return buildErrorResponse(-32603, "Internal error: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public ResponseEntity<JsonRpcResponse> handleAllException(Exception e) {
|
public ResponseEntity<JsonRpcResponse> handleAllException(Exception e) {
|
||||||
log.error(" [WAS Fatal Error] 치명적 오류 발생", e);
|
log.error(" [Gateway Fatal Error] 치명적 오류 발생", e);
|
||||||
return buildErrorResponse(-32000, "Server error: 시스템 관리자에게 문의하세요.");
|
return buildErrorResponse(-32000, "Server error: 시스템 관리자에게 문의하세요.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,244 +0,0 @@
|
|||||||
package io.shinhanlife.dap.lib.presentation;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.mcc.presentation
|
|
||||||
* @className BusinessToolController
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.networknt.schema.Error;
|
|
||||||
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpTool;
|
|
||||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
|
||||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
|
||||||
import io.shinhanlife.dap.lib.mcp.LocalToolScanner;
|
|
||||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.aop.support.AopUtils;
|
|
||||||
import org.springframework.context.ApplicationContext;
|
|
||||||
import org.springframework.core.annotation.AnnotationUtils;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.util.ClassUtils;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class BusinessToolController {
|
|
||||||
|
|
||||||
private final ApplicationContext applicationContext;
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
private final McpProperties mcpProperties;
|
|
||||||
private final LocalToolScanner localToolScanner;
|
|
||||||
private final ToolArgumentSchemaValidator toolArgumentSchemaValidator;
|
|
||||||
|
|
||||||
private final ToolSchemaResolver toolSchemaResolver;
|
|
||||||
// 내부 조회용 로컬 Tool 목록 엔드포인트
|
|
||||||
@GetMapping("/mcp/api/v1/tools/local")
|
|
||||||
public List<ToolMetadata> getLocalTools() {
|
|
||||||
return localToolScanner.getAllScannedTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 순수 REST 기반 동적 라우팅 엔드포인트
|
|
||||||
@PostMapping("/mcp/{name}")
|
|
||||||
public ResponseEntity<?> executeDynamicTool(
|
|
||||||
@PathVariable("name") String functionName,
|
|
||||||
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId,
|
|
||||||
@RequestHeader(value = "trace-id", required = false) String traceId,
|
|
||||||
@RequestHeader(value = "request-id", required = false) String requestId,
|
|
||||||
@RequestHeader(value = "employee-id", required = false) String encryptedEmployeeId,
|
|
||||||
@RequestBody(required = false) Map<String, Object> arguments) {
|
|
||||||
|
|
||||||
String finalRequestId = headerRequestId;
|
|
||||||
|
|
||||||
log.info(" [Tool] IN - trace-id: {}, request-id: {}", traceId, requestId);
|
|
||||||
log.info(" [Tool] 동적 툴 실행 요청 수신 (함수명): {}", functionName);
|
|
||||||
if (arguments != null) {
|
|
||||||
try {
|
|
||||||
log.info(" [Tool] 호출 파라미터: {}", objectMapper.writeValueAsString(arguments));
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.info(" [Tool] 호출 파라미터: {}", arguments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Object targetBean = null;
|
|
||||||
Method targetMethod = null;
|
|
||||||
McpFunction targetFunctionAnnotation = null;
|
|
||||||
|
|
||||||
// McpTool 어노테이션 기반 조회가 프록시 문제로 누락될 수 있으므로, 전체 빈을 순회하며 @McpFunction을 찾습니다.
|
|
||||||
Map<String, Object> allBeans = applicationContext.getBeansOfType(Object.class);
|
|
||||||
outerLoop:
|
|
||||||
for (Object bean : allBeans.values()) {
|
|
||||||
Class<?> targetClass = AopUtils.getTargetClass(bean);
|
|
||||||
for (Method targetMethodOfClass : targetClass.getDeclaredMethods()) {
|
|
||||||
McpFunction mcpFunc = AnnotationUtils.findAnnotation(targetMethodOfClass, McpFunction.class);
|
|
||||||
if (mcpFunc != null) {
|
|
||||||
String baseName = mcpFunc.name();
|
|
||||||
String expectedName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
|
||||||
? mcpProperties.getNamespace() + "_" + baseName
|
|
||||||
: baseName;
|
|
||||||
|
|
||||||
if (expectedName.equals(functionName) || baseName.equals(functionName)) {
|
|
||||||
targetBean = bean;
|
|
||||||
try {
|
|
||||||
targetMethod = bean.getClass().getMethod(targetMethodOfClass.getName(), targetMethodOfClass.getParameterTypes());
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
targetMethod = targetMethodOfClass;
|
|
||||||
}
|
|
||||||
targetFunctionAnnotation = mcpFunc;
|
|
||||||
break outerLoop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetBean == null || targetMethod == null) {
|
|
||||||
List<String> availableFunctions = new ArrayList<>();
|
|
||||||
for (Object bean : allBeans.values()) {
|
|
||||||
Class<?> targetCls = AopUtils.getTargetClass(bean);
|
|
||||||
for (Method m : targetCls.getDeclaredMethods()) {
|
|
||||||
McpFunction func = AnnotationUtils.findAnnotation(m, McpFunction.class);
|
|
||||||
if (func != null) {
|
|
||||||
String baseName = func.name();
|
|
||||||
String expName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
|
||||||
? mcpProperties.getNamespace() + "_" + baseName : baseName;
|
|
||||||
availableFunctions.add(expName + " (in " + targetCls.getSimpleName() + ")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.error("[Tool] 실행할 함수(Method)를 찾을 수 없습니다: {}. 현재 스캔된 툴 메서드 목록: {}", functionName, availableFunctions);
|
|
||||||
Map<String, Object> errorDetails = new HashMap<>();
|
|
||||||
errorDetails.put("status", "404");
|
|
||||||
Map<String, Object> errorBody = new HashMap<>();
|
|
||||||
errorBody.put("code", "TOOL_NOT_FOUND");
|
|
||||||
errorBody.put("message", "실행할 함수를 찾을 수 없습니다: " + functionName);
|
|
||||||
errorBody.put("details", errorDetails);
|
|
||||||
if (finalRequestId != null) errorBody.put("request_id", finalRequestId);
|
|
||||||
|
|
||||||
return ResponseEntity.status(404).body(errorBody);
|
|
||||||
}
|
|
||||||
// (기존 차단 로직 제거됨)
|
|
||||||
|
|
||||||
// 2. 파라미터 유효성 검증 (JSON Schema)
|
|
||||||
if (targetMethod.getParameterCount() > 0) {
|
|
||||||
Class<?> paramType = targetMethod.getParameterTypes()[0];
|
|
||||||
if (!Map.class.isAssignableFrom(paramType)) {
|
|
||||||
try {
|
|
||||||
Map<String, Object> inputSchema = toolSchemaResolver.resolve(targetFunctionAnnotation, paramType);
|
|
||||||
List<Error> errors = toolArgumentSchemaValidator.validate(inputSchema, arguments);
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors);
|
|
||||||
List<String> errorMessages = new ArrayList<>();
|
|
||||||
for (Error validationError : errors) {
|
|
||||||
errorMessages.add(validationError.getMessage());
|
|
||||||
}
|
|
||||||
Map<String, Object> errorDetails = new HashMap<>();
|
|
||||||
errorDetails.put("status", "422");
|
|
||||||
Map<String, Object> errorBody = new HashMap<>();
|
|
||||||
errorBody.put("code", "INVALID_PARAM");
|
|
||||||
errorBody.put("message", "파라미터 유효성 검증 실패");
|
|
||||||
errorBody.put("details", errorDetails);
|
|
||||||
if (finalRequestId != null) errorBody.put("request_id", finalRequestId);
|
|
||||||
|
|
||||||
return ResponseEntity.status(422).body(errorBody);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[Tool] 스키마 검증 중 오류 발생: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("[Tool] 리플렉션 직접 실행 -> Method: {}", targetMethod.getName());
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 3. DTO 파라미터 자동 매핑 (Map -> DTO)
|
|
||||||
Object invokeArgument = arguments;
|
|
||||||
if (targetMethod.getParameterCount() > 0 && arguments != null) {
|
|
||||||
Class<?> paramType = targetMethod.getParameterTypes()[0];
|
|
||||||
if (!Map.class.isAssignableFrom(paramType)) {
|
|
||||||
invokeArgument = objectMapper.convertValue(arguments, paramType);
|
|
||||||
log.info("[Tool] DTO 자동 매핑 성공: {}", paramType.getSimpleName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
long startTime = System.currentTimeMillis();
|
|
||||||
Object methodResult = null;
|
|
||||||
if (targetMethod.getParameterCount() == 0) {
|
|
||||||
methodResult = targetMethod.invoke(targetBean);
|
|
||||||
} else {
|
|
||||||
methodResult = targetMethod.invoke(targetBean, invokeArgument);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(targetFunctionAnnotation, targetMethod.getReturnType());
|
|
||||||
if (!outputSchema.isEmpty()) {
|
|
||||||
List<Error> outputErrors = toolArgumentSchemaValidator.validateValue(outputSchema, methodResult);
|
|
||||||
if (!outputErrors.isEmpty()) {
|
|
||||||
log.error("[Tool] Output schema validation failed. tool={}, errors={}",
|
|
||||||
functionName, outputErrors);
|
|
||||||
Map<String, Object> errorBody = new HashMap<>();
|
|
||||||
errorBody.put("code", "INVALID_TOOL_RESPONSE");
|
|
||||||
errorBody.put("message", "Tool response does not match its output schema");
|
|
||||||
if (finalRequestId != null) {
|
|
||||||
errorBody.put("request_id", finalRequestId);
|
|
||||||
}
|
|
||||||
return ResponseEntity.internalServerError().body(errorBody);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
long elapsed = System.currentTimeMillis() - startTime;
|
|
||||||
|
|
||||||
// 5. 결과 반환 (순수 REST 응답)
|
|
||||||
try {
|
|
||||||
log.info("[Tool Execution] Output Schema Result: {}", objectMapper.writeValueAsString(methodResult));
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.info("[Tool Execution] Output Schema Result: {}", methodResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(" [Tool] OUT - trace-id: {}, request-id: {}", traceId, requestId);
|
|
||||||
|
|
||||||
ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.ok();
|
|
||||||
if (traceId != null) responseBuilder.header("trace-id", traceId);
|
|
||||||
if (requestId != null) responseBuilder.header("request-id", requestId);
|
|
||||||
|
|
||||||
return responseBuilder.body(methodResult);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[Tool] 리플렉션 실행 중 예외 발생: {}", e.getMessage());
|
|
||||||
Map<String, Object> errorDetails = new HashMap<>();
|
|
||||||
errorDetails.put("status", "500");
|
|
||||||
Map<String, Object> errorBody = new HashMap<>();
|
|
||||||
errorBody.put("code", "TOOL_ERROR");
|
|
||||||
errorBody.put("message", "Tool execution failed");
|
|
||||||
errorDetails.clear(); // Hide details for upstream errors
|
|
||||||
errorBody.put("details", errorDetails);
|
|
||||||
if (finalRequestId != null) errorBody.put("request_id", finalRequestId);
|
|
||||||
|
|
||||||
return ResponseEntity.status(502).body(errorBody);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package io.shinhanlife.dap.lib.presentation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.presentation
|
|
||||||
* @className ToolManifestController
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.manifest.ToolManifestResponse;
|
|
||||||
import io.shinhanlife.dap.lib.manifest.ToolManifestService;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
/** Read-only Tool Service manifest endpoint for MCP background discovery. */
|
|
||||||
@RestController
|
|
||||||
public class ToolManifestController {
|
|
||||||
|
|
||||||
private final ToolManifestService toolManifestService;
|
|
||||||
|
|
||||||
public ToolManifestController(ToolManifestService toolManifestService) {
|
|
||||||
this.toolManifestService = toolManifestService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping(value = "/tool-manifest", produces = MediaType.APPLICATION_JSON_VALUE)
|
|
||||||
public ResponseEntity<ToolManifestResponse> getManifest(
|
|
||||||
@RequestHeader(value = HttpHeaders.IF_NONE_MATCH, required = false) String ifNoneMatch) {
|
|
||||||
ToolManifestResponse manifest = toolManifestService.currentManifest();
|
|
||||||
String eTag = '"' + manifest.revision() + '"';
|
|
||||||
if (eTag.equals(ifNoneMatch)) {
|
|
||||||
return ResponseEntity.status(304).eTag(eTag).build();
|
|
||||||
}
|
|
||||||
return ResponseEntity.ok().eTag(eTag).body(manifest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package io.shinhanlife.dap.lib.session.presentation;
|
|
||||||
|
|
||||||
import io.micrometer.common.util.StringUtils;
|
|
||||||
import io.shinhanlife.glow.BaseResponse;
|
|
||||||
import io.shinhanlife.glow.BizException;
|
|
||||||
import io.shinhanlife.glow.GlowControllerId;
|
|
||||||
import io.shinhanlife.glow.ResponseUtil;
|
|
||||||
import io.shinhanlife.dap.lib.session.dto.SessionDto;
|
|
||||||
import io.shinhanlife.dap.lib.session.presentation.io.SsoResponse;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import jakarta.servlet.http.HttpSession;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.ibatis.javassist.NotFoundException;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.session.presentation
|
|
||||||
* @className SsoRestController
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
@RestController
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Slf4j
|
|
||||||
@RequestMapping("/sso")
|
|
||||||
public class SsoRestController {
|
|
||||||
|
|
||||||
private static final String NLS_LOGIN_URL = "";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* sso 연동 전 임시 로그인
|
|
||||||
*
|
|
||||||
* @param request
|
|
||||||
* @param response
|
|
||||||
* @param session
|
|
||||||
* @param <T>
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@GlowControllerId("tempLogin")
|
|
||||||
@PostMapping("/tempLogin")
|
|
||||||
public <T> ResponseEntity<BaseResponse<SsoResponse>> tempLogin(HttpServletRequest request, HttpServletResponse response,
|
|
||||||
HttpSession session, @RequestBody SessionDto requestDto) {
|
|
||||||
try {
|
|
||||||
if (StringUtils.isEmpty(requestDto.getPrafNo())) {
|
|
||||||
throw new NotFoundException("SSO >> not found sso id");
|
|
||||||
}
|
|
||||||
|
|
||||||
// DB 조회(ZtUsac) 없이 파라미터로 받은 SessionDto를 그대로 사용
|
|
||||||
SessionDto sessionDto = requestDto;
|
|
||||||
sessionDto.setLoginDtm(); // 로그인 시점 세팅
|
|
||||||
session.setAttribute("userInfo", sessionDto);
|
|
||||||
return ResponseUtil.ok(SsoResponse.builder().retCode("0").userInfo(sessionDto).build());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("로그인 실패", e);
|
|
||||||
session.invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseUtil.ok(SsoResponse.builder().redirectUrl(NLS_LOGIN_URL).build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package io.shinhanlife.dap.lib.session.presentation.io;
|
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.session.dto.SessionDto;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.session.presentation.io
|
|
||||||
* @className SsoResponse
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class SsoResponse {
|
|
||||||
|
|
||||||
private String retCode;
|
|
||||||
private SessionDto userInfo;
|
|
||||||
private String redirectUrl;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,8 @@ package io.shinhanlife.dap.lib.util;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpAnyOf;
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.ParameterizedType;
|
import java.lang.reflect.ParameterizedType;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
@@ -56,7 +55,7 @@ public class JsonSchemaGenerator {
|
|||||||
// 1. 타입 매핑
|
// 1. 타입 매핑
|
||||||
|
|
||||||
// 2. 어노테이션 기반 설명 추출
|
// 2. 어노테이션 기반 설명 추출
|
||||||
McpParameter paramAnnotation = field.getAnnotation(McpParameter.class);
|
McpToolParam paramAnnotation = field.getAnnotation(McpToolParam.class);
|
||||||
JsonPropertyDescription descAnnotation = field.getAnnotation(JsonPropertyDescription.class);
|
JsonPropertyDescription descAnnotation = field.getAnnotation(JsonPropertyDescription.class);
|
||||||
if (paramAnnotation != null && !paramAnnotation.description().isEmpty()) {
|
if (paramAnnotation != null && !paramAnnotation.description().isEmpty()) {
|
||||||
fieldSchema.put("description", paramAnnotation.description());
|
fieldSchema.put("description", paramAnnotation.description());
|
||||||
@@ -72,46 +71,54 @@ public class JsonSchemaGenerator {
|
|||||||
requiredList.add(field.getName());
|
requiredList.add(field.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
McpValidation validation = field.getAnnotation(McpValidation.class);
|
Schema schemaAnnotation = field.getAnnotation(Schema.class);
|
||||||
if (validation != null && validation.required() && !requiredList.contains(field.getName())) {
|
if (schemaAnnotation != null) {
|
||||||
requiredList.add(field.getName());
|
if (!schemaAnnotation.description().isEmpty() && !fieldSchema.containsKey("description")) {
|
||||||
|
fieldSchema.put("description", schemaAnnotation.description());
|
||||||
|
}
|
||||||
|
if (schemaAnnotation.required() && !requiredList.contains(field.getName())) {
|
||||||
|
requiredList.add(field.getName());
|
||||||
|
}
|
||||||
|
if (!schemaAnnotation.pattern().isEmpty()) {
|
||||||
|
fieldSchema.put("pattern", schemaAnnotation.pattern());
|
||||||
|
}
|
||||||
|
if (!schemaAnnotation.minimum().isEmpty()) {
|
||||||
|
try {
|
||||||
|
fieldSchema.put("minimum", Long.valueOf(schemaAnnotation.minimum()));
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
if (!schemaAnnotation.maximum().isEmpty()) {
|
||||||
|
try {
|
||||||
|
fieldSchema.put("maximum", Long.valueOf(schemaAnnotation.maximum()));
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
if (schemaAnnotation.minLength() > 0) {
|
||||||
|
fieldSchema.put("minLength", schemaAnnotation.minLength());
|
||||||
|
}
|
||||||
|
if (schemaAnnotation.maxLength() > 0 && schemaAnnotation.maxLength() != Integer.MAX_VALUE) {
|
||||||
|
fieldSchema.put("maxLength", schemaAnnotation.maxLength());
|
||||||
|
}
|
||||||
|
if (schemaAnnotation.allowableValues().length > 0 && !schemaAnnotation.allowableValues()[0].isEmpty()) {
|
||||||
|
fieldSchema.put("enum", List.of(schemaAnnotation.allowableValues()));
|
||||||
|
}
|
||||||
|
if (!schemaAnnotation.format().isEmpty()) {
|
||||||
|
fieldSchema.put("format", schemaAnnotation.format());
|
||||||
|
}
|
||||||
|
if (!schemaAnnotation.defaultValue().isEmpty()) {
|
||||||
|
fieldSchema.put("default", coerceDefaultValue(schemaAnnotation.defaultValue(), field.getType()));
|
||||||
|
}
|
||||||
|
if (!schemaAnnotation.example().isEmpty()) {
|
||||||
|
fieldSchema.put("examples", List.of(schemaAnnotation.example()));
|
||||||
|
}
|
||||||
|
if (schemaAnnotation.nullable()) {
|
||||||
|
Map<String, Object> nonNullSchema = new HashMap<>(fieldSchema);
|
||||||
|
fieldSchema = new HashMap<>();
|
||||||
|
fieldSchema.put("anyOf", List.of(
|
||||||
|
nonNullSchema,
|
||||||
|
Map.of("type", "null")
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (validation != null && !validation.pattern().isEmpty()) {
|
|
||||||
fieldSchema.put("pattern", validation.pattern());
|
|
||||||
}
|
|
||||||
if (validation != null && validation.minimum() != Long.MIN_VALUE) {
|
|
||||||
fieldSchema.put("minimum", validation.minimum());
|
|
||||||
}
|
|
||||||
if (validation != null && validation.maximum() != Long.MAX_VALUE) {
|
|
||||||
fieldSchema.put("maximum", validation.maximum());
|
|
||||||
}
|
|
||||||
if (validation != null && validation.minLength() >= 0) {
|
|
||||||
fieldSchema.put("minLength", validation.minLength());
|
|
||||||
}
|
|
||||||
if (validation != null && validation.maxLength() >= 0) {
|
|
||||||
fieldSchema.put("maxLength", validation.maxLength());
|
|
||||||
}
|
|
||||||
if (validation != null && validation.allowedValues().length > 0) {
|
|
||||||
fieldSchema.put("enum", List.of(validation.allowedValues()));
|
|
||||||
}
|
|
||||||
if (validation != null && !validation.format().isEmpty()) {
|
|
||||||
fieldSchema.put("format", validation.format());
|
|
||||||
}
|
|
||||||
if (validation != null && !validation.defaultValue().isEmpty()) {
|
|
||||||
fieldSchema.put("default", coerceDefaultValue(validation.defaultValue(), field.getType()));
|
|
||||||
}
|
|
||||||
if (validation != null && validation.examples().length > 0) {
|
|
||||||
fieldSchema.put("examples", List.of(validation.examples()));
|
|
||||||
}
|
|
||||||
if (validation != null && validation.nullable()) {
|
|
||||||
Map<String, Object> nonNullSchema = new HashMap<>(fieldSchema);
|
|
||||||
fieldSchema = new HashMap<>();
|
|
||||||
fieldSchema.put("anyOf", List.of(
|
|
||||||
nonNullSchema,
|
|
||||||
Map.of("type", "null")
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
properties.put(field.getName(), fieldSchema);
|
properties.put(field.getName(), fieldSchema);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,15 +127,7 @@ public class JsonSchemaGenerator {
|
|||||||
schema.put("required", requiredList);
|
schema.put("required", requiredList);
|
||||||
}
|
}
|
||||||
|
|
||||||
McpAnyOf anyOfAnnotation = clazz.getAnnotation(McpAnyOf.class);
|
// anyOf removed
|
||||||
if (anyOfAnnotation != null && anyOfAnnotation.value().length > 0) {
|
|
||||||
List<Map<String, Object>> anyOfList = new ArrayList<>();
|
|
||||||
for (String fieldName : anyOfAnnotation.value()) {
|
|
||||||
anyOfList.add(Map.of("required", List.of(fieldName)));
|
|
||||||
|
|
||||||
}
|
|
||||||
schema.put("anyOf", anyOfList);
|
|
||||||
}
|
|
||||||
|
|
||||||
visiting.remove(clazz);
|
visiting.remove(clazz);
|
||||||
return schema;
|
return schema;
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
package io.shinhanlife.dap.lib.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
|
public class PodScaffolder {
|
||||||
|
|
||||||
|
public static void main(String[] args) throws IOException {
|
||||||
|
Scanner scanner = new Scanner(System.in);
|
||||||
|
|
||||||
|
System.out.println("=========================================");
|
||||||
|
System.out.println(" MCP Tool Pod Scaffolder (Java CLI) ");
|
||||||
|
System.out.println("=========================================\n");
|
||||||
|
|
||||||
|
String rawModuleName = getOrAsk(args, 0, scanner, "1. 생성할 모듈(Pod) 이름 (예: payment 또는 dap-was-payment): ");
|
||||||
|
String moduleName = rawModuleName.startsWith("dap-was-") ? rawModuleName : "dap-was-" + rawModuleName;
|
||||||
|
String portStr = getOrAsk(args, 1, scanner, "2. 사용할 포트 번호 (예: 8085): ");
|
||||||
|
String shortName = moduleName.replace("dap-was-", "").replace("-", "");
|
||||||
|
|
||||||
|
String defaultAuthor = System.getProperty("user.name");
|
||||||
|
String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
|
||||||
|
|
||||||
|
String author = getOrAsk(args, 2, scanner, "3. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): ");
|
||||||
|
if (author.trim().isEmpty()) author = defaultAuthor;
|
||||||
|
String createDate = getOrAsk(args, 3, scanner, "4. 작성일 (엔터 입력 시 '" + defaultDate + "'): ");
|
||||||
|
if (createDate.trim().isEmpty()) createDate = defaultDate;
|
||||||
|
|
||||||
|
String result = scaffoldPod(moduleName, portStr, shortName, author, createDate);
|
||||||
|
System.out.println(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) {
|
||||||
|
if (args.length > index) {
|
||||||
|
return args[index];
|
||||||
|
}
|
||||||
|
System.out.print(prompt);
|
||||||
|
return scanner.nextLine().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String scaffoldPod(String moduleName, String portStr, String shortName, String author, String createDate) throws IOException {
|
||||||
|
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
|
||||||
|
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
|
||||||
|
|
||||||
|
Path modulePath = rootDir.resolve(Paths.get(moduleName));
|
||||||
|
if (Files.exists(modulePath)) {
|
||||||
|
return "[오류] 이미 존재하는 모듈입니다: " + moduleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder log = new StringBuilder();
|
||||||
|
log.append("[1/6] 모듈 디렉터리 생성 중...\n");
|
||||||
|
Files.createDirectories(modulePath);
|
||||||
|
|
||||||
|
log.append("[2/6] build.gradle 생성 중...\n");
|
||||||
|
String buildGradle = """
|
||||||
|
plugins {
|
||||||
|
id 'org.springframework.boot'
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
implementation project(':dap-was-lib')
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||||
|
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Files.writeString(modulePath.resolve("build.gradle"), buildGradle);
|
||||||
|
|
||||||
|
log.append("[3/6] Dockerfile 생성 중...\n");
|
||||||
|
String dockerfile = """
|
||||||
|
FROM eclipse-temurin:21-jdk-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY build/libs/%s-0.0.1-SNAPSHOT.jar app.jar
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
|
""".formatted(moduleName);
|
||||||
|
Files.writeString(modulePath.resolve("Dockerfile"), dockerfile);
|
||||||
|
|
||||||
|
log.append("[4/6] Application 클래스 및 설정 파일 생성 중...\n");
|
||||||
|
Path srcPath = modulePath.resolve("src/main/java/io/shinhanlife/dap/mcc/" + shortName);
|
||||||
|
Files.createDirectories(srcPath);
|
||||||
|
|
||||||
|
String appClass = """
|
||||||
|
package io.shinhanlife.dap.mcc.%s;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.%s
|
||||||
|
* @className DapWas%sApplication
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author %s
|
||||||
|
* @create %s
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.lib"})
|
||||||
|
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.lib"})
|
||||||
|
@EnableCaching
|
||||||
|
public class DapWas%sApplication {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(DapWas%sApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".formatted(shortName, shortName, capitalize(shortName), author, createDate, createDate, author, capitalize(shortName), capitalize(shortName));
|
||||||
|
Files.writeString(srcPath.resolve("DapWas" + capitalize(shortName) + "Application.java"), appClass);
|
||||||
|
|
||||||
|
Path resPath = modulePath.resolve("src/main/resources");
|
||||||
|
Files.createDirectories(resPath);
|
||||||
|
String applicationYml = """
|
||||||
|
server:
|
||||||
|
port: %s
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: %s
|
||||||
|
profiles:
|
||||||
|
active: local
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
org.apache.kafka: ERROR
|
||||||
|
mcp:
|
||||||
|
namespace: ""
|
||||||
|
security:
|
||||||
|
tenant-domains:
|
||||||
|
TESTER-DEV: ALL
|
||||||
|
""".formatted(portStr, moduleName);
|
||||||
|
Files.writeString(resPath.resolve("application.yml"), applicationYml);
|
||||||
|
|
||||||
|
String applicationLocalYml = """
|
||||||
|
# Local 환경 전용 설정 (H2 메모리 DB 등)
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1;
|
||||||
|
driverClassName: com.p6spy.engine.spy.P6SpyDriver
|
||||||
|
username: sa
|
||||||
|
password: password
|
||||||
|
h2:
|
||||||
|
console:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
eims:
|
||||||
|
http:
|
||||||
|
url: http://localhost:${server.port}/api/gateway
|
||||||
|
tcp:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8090
|
||||||
|
timeout: 5000
|
||||||
|
jsp:
|
||||||
|
form:
|
||||||
|
url: http://localhost:${server.port}/mock/jsp-form
|
||||||
|
json:
|
||||||
|
url: http://localhost:${server.port}/mock/jsp-json
|
||||||
|
mci:
|
||||||
|
url: http://localhost:${server.port}/api/mock/esb/api
|
||||||
|
mcistring:
|
||||||
|
url: http://localhost:${server.port}/api/mock/esb/string
|
||||||
|
|
||||||
|
mcp:
|
||||||
|
security:
|
||||||
|
tenant-domains:
|
||||||
|
mcp-client-1: CUSTOMER,COMMON
|
||||||
|
mcp-client-2: ALL
|
||||||
|
|
||||||
|
axhub:
|
||||||
|
gateway:
|
||||||
|
url: http://localhost:8081
|
||||||
|
tool:
|
||||||
|
url: ${AXHUB_TOOL_URL:http://localhost:${server.port}}
|
||||||
|
""";
|
||||||
|
Files.writeString(resPath.resolve("application-local.yml"), applicationLocalYml);
|
||||||
|
|
||||||
|
String applicationDevYml = """
|
||||||
|
# OCI 클라우드 환경 전용 설정
|
||||||
|
server:
|
||||||
|
port: ${PORT:%s}
|
||||||
|
|
||||||
|
axhub:
|
||||||
|
gateway:
|
||||||
|
url: https://axhubmcp.devjun.net
|
||||||
|
tool:
|
||||||
|
url: http://144.24.70.100:%s
|
||||||
|
|
||||||
|
eims:
|
||||||
|
http:
|
||||||
|
url: http://localhost:${server.port}/api/gateway
|
||||||
|
tcp:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8090
|
||||||
|
timeout: 5000
|
||||||
|
jsp:
|
||||||
|
form:
|
||||||
|
url: http://localhost:${server.port}/mock/jsp-form
|
||||||
|
json:
|
||||||
|
url: http://localhost:${server.port}/mock/jsp-json
|
||||||
|
mci:
|
||||||
|
url: http://localhost:${server.port}/api/mock/esb/api
|
||||||
|
mcistring:
|
||||||
|
url: http://localhost:${server.port}/api/mock/esb/string
|
||||||
|
|
||||||
|
shinhan:
|
||||||
|
integration:
|
||||||
|
envrTypeCd: D
|
||||||
|
eai:
|
||||||
|
url: http://10.176.32.181
|
||||||
|
internalMci:
|
||||||
|
url: http://10.176.32.173
|
||||||
|
bancaMci:
|
||||||
|
url: http://10.176.32.117
|
||||||
|
externalMci:
|
||||||
|
url: http://10.176.32.176
|
||||||
|
""".formatted(portStr, portStr);
|
||||||
|
Files.writeString(resPath.resolve("application-dev.yml"), applicationDevYml);
|
||||||
|
|
||||||
|
String logbackXml = """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration>
|
||||||
|
<property name="LOG_PATTERN" value="%%d{yyyy-MM-dd HH:mm:ss.SSS} [%%thread] [%%X{traceId}] %%-5level %%logger{36} - %%msg%%n" />
|
||||||
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>${LOG_PATTERN}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>logs/%s.log</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>logs/%s-%%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>${LOG_PATTERN}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="CONSOLE" />
|
||||||
|
<appender-ref ref="FILE" />
|
||||||
|
</root>
|
||||||
|
<logger name="io.shinhanlife" level="DEBUG" />
|
||||||
|
</configuration>
|
||||||
|
""".formatted(moduleName, moduleName);
|
||||||
|
Files.writeString(resPath.resolve("logback-spring.xml"), logbackXml);
|
||||||
|
|
||||||
|
log.append("[5/6] settings.gradle 에 모듈 등록 중...\n");
|
||||||
|
Path settingsPath = rootDir.resolve(Paths.get("settings.gradle"));
|
||||||
|
if (Files.exists(settingsPath)) {
|
||||||
|
String settings = Files.readString(settingsPath);
|
||||||
|
if (!settings.contains("include '" + moduleName + "'")) {
|
||||||
|
Files.writeString(settingsPath, System.lineSeparator() + "include '" + moduleName + "'" + System.lineSeparator(), StandardOpenOption.APPEND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.append("[6/6] docker-compose.yml 에 서비스 추가 중...\n");
|
||||||
|
Path dockerComposePath = rootDir.resolve(Paths.get("docker-compose.yml"));
|
||||||
|
if (Files.exists(dockerComposePath)) {
|
||||||
|
String compose = Files.readString(dockerComposePath);
|
||||||
|
String serviceName = moduleName.replace("dap-", ""); // e.g. tool-payment
|
||||||
|
if (!compose.contains(" " + serviceName + ":")) {
|
||||||
|
String newService = """
|
||||||
|
%s:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: %s/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "%s:%s"
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Seoul
|
||||||
|
- SPRING_REDIS_HOST=redis
|
||||||
|
- SPRING_REDIS_PORT=6379
|
||||||
|
- SPRING_DATA_REDIS_PORT=6379
|
||||||
|
- AXHUB_GATEWAY_URL=http://gateway:8081
|
||||||
|
- AXHUB_TOOL_URL=http://%s:%s
|
||||||
|
- GLOW_COMMUNICATION_MCI_HOST=http://mci-mock
|
||||||
|
- GLOW_COMMUNICATION_MCI_PORT=8080
|
||||||
|
- GLOW_COMMUNICATION_EXTMCI_HOST=http://mci-mock
|
||||||
|
- GLOW_COMMUNICATION_EXTMCI_PORT=8080
|
||||||
|
- GLOW_COMMUNICATION_EAI_HOST=http://mci-mock
|
||||||
|
- GLOW_COMMUNICATION_EAI_PORT=8080
|
||||||
|
""".formatted(serviceName, moduleName, portStr, portStr, serviceName, portStr);
|
||||||
|
Files.writeString(dockerComposePath, System.lineSeparator() + newService, StandardOpenOption.APPEND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.append("\n=========================================\n");
|
||||||
|
log.append(" Pod Scaffolding Complete! \n");
|
||||||
|
log.append("=========================================\n");
|
||||||
|
log.append("1. [새로운 모듈] ").append(moduleName).append(" 폴더가 생성되었습니다.\n");
|
||||||
|
log.append("2. [ToolScaffolder]를 사용해 이 모듈 안에 툴을 추가하세요.\n");
|
||||||
|
log.append("3. 실행 전 Gradle 동기화(Sync)를 한 번 진행해 주세요.\n");
|
||||||
|
return log.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String capitalize(String str) {
|
||||||
|
if (str == null || str.isEmpty()) return str;
|
||||||
|
return str.substring(0, 1).toUpperCase() + str.substring(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,783 @@
|
|||||||
|
package io.shinhanlife.dap.lib.util;
|
||||||
|
|
||||||
|
|
||||||
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스
|
||||||
|
*
|
||||||
|
* [실행 방법]
|
||||||
|
* 방법 1. IDE(IntelliJ 등)에서 직접 실행 (대화형 모드 추천 ⭐)
|
||||||
|
* - 이 클래스(ToolScaffolder.java)를 열고 main 메서드를 직접 실행(Run)합니다.
|
||||||
|
* - 콘솔 창에 뜨는 질문에 차례대로 값을 입력하기만 하면 파일이 생성됩니다.
|
||||||
|
*
|
||||||
|
* 방법 2. 커맨드라인(터미널)에서 실행 (명령어 기반)
|
||||||
|
* - 컴파일: javac -encoding UTF-8 dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/util/ToolScaffolder.java
|
||||||
|
* - 실행: java -cp dap-was-lib/src/main/java io.shinhanlife.dap.lib.util.ToolScaffolder [이름] [ID] "[설명]" "[그룹]" "[통신방식]" "[모듈명]"
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.lib.util
|
||||||
|
* @className ToolScaffolder
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 0986406
|
||||||
|
* @create 2026.09.01
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.09.01 0986406 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public class ToolScaffolder {
|
||||||
|
|
||||||
|
private static final String BASE_PACKAGE = "io.shinhanlife.dap.mcc";
|
||||||
|
private static final String BASE_PACKAGE_PATH = "src/main/java/io/shinhanlife/dap/mcc";
|
||||||
|
|
||||||
|
public static void main(String[] args) throws IOException {
|
||||||
|
Scanner scanner = new Scanner(System.in);
|
||||||
|
|
||||||
|
System.out.println("=========================================");
|
||||||
|
System.out.println(" MCP Tool Scaffolder (Java CLI) ");
|
||||||
|
System.out.println("=========================================\n");
|
||||||
|
|
||||||
|
String baseName = getOrAsk(args, 0, scanner, "1. 생성할 Tool의 기본 이름 (예: ExchangeRate) [영문 PascalCase]: ");
|
||||||
|
String interfaceId = getOrAsk(args, 1, scanner, "2. 레거시 API 인터페이스 ID (예: EXCH_001): ");
|
||||||
|
String description = getOrAsk(args, 2, scanner, "3. Tool 기능 설명 (예: 환율 조회): ");
|
||||||
|
String group = getOrAsk(args, 3, scanner, "4. Tool 소속 그룹 (예: SAMPLE, NOTIFICATION, CLAIM, POLICY, HR, CONTRACT, CUSTOMER 등): ");
|
||||||
|
if (group.isEmpty()) group = "COMMON";
|
||||||
|
String routingType = getOrAsk(args, 4, scanner, "5. 통신 프로토콜 (예: HTTP, TCP, MCI, EAI): ");
|
||||||
|
if (routingType.trim().isEmpty()) {
|
||||||
|
routingType = "HTTP";
|
||||||
|
}
|
||||||
|
String moduleName = getOrAsk(args, 5, scanner, "6. 코드를 생성할 모듈 (기본: dap-was-oth): ");
|
||||||
|
if (moduleName.trim().isEmpty()) {
|
||||||
|
moduleName = "dap-was-oth";
|
||||||
|
}
|
||||||
|
|
||||||
|
String defaultAuthor = System.getProperty("user.name");
|
||||||
|
String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
|
||||||
|
|
||||||
|
String author = getOrAsk(args, 6, scanner, "7. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): ");
|
||||||
|
if (author.trim().isEmpty()) author = defaultAuthor;
|
||||||
|
String createDate = getOrAsk(args, 7, scanner, "8. 작성일 (엔터 입력 시 '" + defaultDate + "'): ");
|
||||||
|
if (createDate.trim().isEmpty()) createDate = defaultDate;
|
||||||
|
|
||||||
|
String useSchemaResourceStr = getOrAsk(args, 8, scanner, "9. input/output JSON Schema 파일 자동 생성 여부 (y/N): ");
|
||||||
|
boolean useSchemaResource = "y".equalsIgnoreCase(useSchemaResourceStr.trim());
|
||||||
|
|
||||||
|
String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, true, null, useSchemaResource);
|
||||||
|
System.out.println(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) {
|
||||||
|
if (args.length > index) {
|
||||||
|
return args[index];
|
||||||
|
}
|
||||||
|
System.out.print(prompt);
|
||||||
|
return scanner.nextLine().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode) throws IOException {
|
||||||
|
return scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, register, clientSystemCode, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, boolean useSchemaResource) throws IOException {
|
||||||
|
baseName = toPascalCase(baseName);
|
||||||
|
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
|
||||||
|
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
|
||||||
|
|
||||||
|
Path usecaseDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "usecase"));
|
||||||
|
Path usecaseImplDir = usecaseDir.resolve("impl");
|
||||||
|
Path dtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "dto"));
|
||||||
|
|
||||||
|
Path legacyDtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "legacy"));
|
||||||
|
Path converterDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "converter"));
|
||||||
|
|
||||||
|
// schema resource 파일 경로 (useSchemaResource=true 일 때만 생성)
|
||||||
|
String schemaBaseName = toKebabCase(baseName);
|
||||||
|
String inputSchemaFileName = schemaBaseName + "-resource-input-schema.json";
|
||||||
|
String outputSchemaFileName = schemaBaseName + "-resource-output-schema.json";
|
||||||
|
Path schemaDir = rootDir.resolve(Paths.get(moduleName, "src/main/resources/mcp/schema"));
|
||||||
|
String inputSchemaClasspath = "classpath:mcp/schema/" + inputSchemaFileName;
|
||||||
|
String outputSchemaClasspath = "classpath:mcp/schema/" + outputSchemaFileName;
|
||||||
|
|
||||||
|
String bizPackage = BASE_PACKAGE + ".biz." + group.toLowerCase();
|
||||||
|
boolean isMci = "MCI".equalsIgnoreCase(routingType);
|
||||||
|
String mciGroupPath = "infra/itrf/mci/" + group.toLowerCase();
|
||||||
|
String clientPkgSuffix = "";
|
||||||
|
String clientPrefixCap = "";
|
||||||
|
Path mciClientDir = null;
|
||||||
|
|
||||||
|
if (isMci && clientSystemCode != null && clientSystemCode.length() == 4) {
|
||||||
|
String clientPrefix = clientSystemCode.toLowerCase();
|
||||||
|
clientPkgSuffix = clientPrefix.substring(0, 3) + "." + clientPrefix.substring(3, 4);
|
||||||
|
clientPrefixCap = toPascalCase(clientSystemCode);
|
||||||
|
mciGroupPath = "infra/itrf/mci/" + clientPrefix.substring(0, 3) + "/" + clientPrefix.substring(3, 4);
|
||||||
|
mciClientDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
Path mciIoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath, "io"));
|
||||||
|
|
||||||
|
Files.createDirectories(usecaseDir);
|
||||||
|
Files.createDirectories(usecaseImplDir);
|
||||||
|
Files.createDirectories(dtoDir);
|
||||||
|
if (isMci) {
|
||||||
|
Files.createDirectories(mciIoDir);
|
||||||
|
if (mciClientDir != null) {
|
||||||
|
Files.createDirectories(mciClientDir);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Files.createDirectories(legacyDtoDir);
|
||||||
|
}
|
||||||
|
Files.createDirectories(converterDir);
|
||||||
|
|
||||||
|
StringBuilder log = new StringBuilder();
|
||||||
|
|
||||||
|
// Generate Request DTO
|
||||||
|
String reqContent = """
|
||||||
|
package %s.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package %s.dto
|
||||||
|
* @className %sRequest
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author %s
|
||||||
|
* @create %s
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class %sRequest {
|
||||||
|
@McpToolParam(description = "수신자 전화번호", required = true)
|
||||||
|
-(?:\\\\d{3}|\\\\d{4})-\\\\d{4}$", examples = {"010-1234-5678"})
|
||||||
|
private String phoneNumber;
|
||||||
|
|
||||||
|
@McpToolParam(description = "전송할 메시지 내용", required = true)
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
|
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||||
|
Files.writeString(dtoDir.resolve(baseName + "Request.java"), reqContent);
|
||||||
|
|
||||||
|
// Generate Response DTO
|
||||||
|
String resContent = """
|
||||||
|
package %s.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package %s.dto
|
||||||
|
* @className %sResponse
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author %s
|
||||||
|
* @create %s
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class %sResponse {
|
||||||
|
private String resultCode;
|
||||||
|
|
||||||
|
private String resultMessage;
|
||||||
|
|
||||||
|
// TODO: Add response fields here. Do not include PII in the Tool response.
|
||||||
|
}
|
||||||
|
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||||
|
Files.writeString(dtoDir.resolve(baseName + "Response.java"), resContent);
|
||||||
|
|
||||||
|
String toolName = toToolName(moduleName, group, baseName);
|
||||||
|
|
||||||
|
String toolHintLine;
|
||||||
|
if (useSchemaResource) {
|
||||||
|
toolHintLine = " @ToolHint(register = %s,\n" +
|
||||||
|
" inputSchemaResource = \"%s\",\n" +
|
||||||
|
" outputSchemaResource = \"%s\")".formatted(register, inputSchemaClasspath, outputSchemaClasspath);
|
||||||
|
} else {
|
||||||
|
toolHintLine = " @ToolHint(register = %s)".formatted(register);
|
||||||
|
}
|
||||||
|
|
||||||
|
String serviceInterfaceContent = """
|
||||||
|
package %s.usecase;
|
||||||
|
|
||||||
|
import org.springframework.ai.mcp.annotation.McpTool;
|
||||||
|
import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||||
|
import %s.dto.%sRequest;
|
||||||
|
import %s.dto.%sResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package %s.usecase
|
||||||
|
* @className %sUseCase
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author %s
|
||||||
|
* @create %s
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public interface %sUseCase {
|
||||||
|
|
||||||
|
@McpTool(name = "%s", title = "%s", description = "%s")
|
||||||
|
%s
|
||||||
|
%sResponse execute(%sRequest req);
|
||||||
|
}
|
||||||
|
""".formatted(
|
||||||
|
bizPackage,
|
||||||
|
bizPackage, baseName,
|
||||||
|
bizPackage, baseName,
|
||||||
|
bizPackage, baseName, author, createDate, createDate, author,
|
||||||
|
baseName,
|
||||||
|
toolName, description, description,
|
||||||
|
toolHintLine,
|
||||||
|
baseName, baseName
|
||||||
|
);
|
||||||
|
|
||||||
|
Files.writeString(usecaseDir.resolve(baseName + "UseCase.java"), serviceInterfaceContent);
|
||||||
|
|
||||||
|
String serviceImplContent;
|
||||||
|
|
||||||
|
if (isMci) {
|
||||||
|
serviceImplContent = """
|
||||||
|
package %s.usecase.impl;
|
||||||
|
|
||||||
|
import %s.dto.%sRequest;
|
||||||
|
import %s.dto.%sResponse;
|
||||||
|
import %s.usecase.%sUseCase;
|
||||||
|
import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent;
|
||||||
|
import io.shinhanlife.glow.communication.dto.Transfer;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import java.util.Map;
|
||||||
|
import %s.converter.%sConverter;
|
||||||
|
import %s.%s.io.%s_I;
|
||||||
|
%s
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package %s.usecase.impl
|
||||||
|
* @className %sUseCaseImpl
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author %s
|
||||||
|
* @create %s
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class %sUseCaseImpl implements %sUseCase {
|
||||||
|
|
||||||
|
%s
|
||||||
|
private final %sConverter converter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object execute(%sRequest req) {
|
||||||
|
log.info("[MCI Tool] {} 요청 수신.", "%s");
|
||||||
|
try {
|
||||||
|
// MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO)
|
||||||
|
%s_I mciReq = converter.toLegacyRequest(req);
|
||||||
|
|
||||||
|
Transfer<Object> resTransfer = mci.callTo(
|
||||||
|
"%s",
|
||||||
|
null,
|
||||||
|
mciReq,
|
||||||
|
Object.class
|
||||||
|
);
|
||||||
|
return resTransfer.getBody() != null ? resTransfer.getBody() : Map.of("status", "SUCCESS");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
|
||||||
|
return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".formatted(
|
||||||
|
bizPackage,
|
||||||
|
bizPackage, baseName,
|
||||||
|
bizPackage, baseName,
|
||||||
|
bizPackage, baseName,
|
||||||
|
bizPackage, baseName,
|
||||||
|
BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId,
|
||||||
|
(clientPrefixCap.isEmpty() ? "" : "import " + BASE_PACKAGE + "." + mciGroupPath.replace("/", ".") + ".Mci" + clientPrefixCap + "Client;\n"),
|
||||||
|
bizPackage,
|
||||||
|
baseName,
|
||||||
|
author,
|
||||||
|
createDate,
|
||||||
|
createDate, author,
|
||||||
|
baseName,
|
||||||
|
baseName,
|
||||||
|
(clientPrefixCap.isEmpty() ? "private final AxhubMciComponent mci;" : "private final Mci" + clientPrefixCap + "Client mci;"),
|
||||||
|
baseName,
|
||||||
|
baseName,
|
||||||
|
toolName,
|
||||||
|
interfaceId,
|
||||||
|
interfaceId
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
serviceImplContent = """
|
||||||
|
package %s.usecase.impl;
|
||||||
|
|
||||||
|
import %s.dto.%sRequest;
|
||||||
|
import %s.dto.%sResponse;
|
||||||
|
import %s.usecase.%sUseCase;
|
||||||
|
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
|
||||||
|
import %s.converter.%sConverter;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package %s.usecase.impl
|
||||||
|
* @className %sUseCaseImpl
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author %s
|
||||||
|
* @create %s
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class %sUseCaseImpl extends AbstractMcpToolUseCase implements %sUseCase {
|
||||||
|
|
||||||
|
private final %sConverter converter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object execute(%sRequest req) {
|
||||||
|
// %sLegacyRequest legacyRequest = converter.toLegacyRequest(req);
|
||||||
|
return executeLegacy("%s", "%s", req); // Or pass legacyRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".formatted(
|
||||||
|
bizPackage,
|
||||||
|
bizPackage, baseName,
|
||||||
|
bizPackage, baseName,
|
||||||
|
bizPackage, baseName,
|
||||||
|
bizPackage, baseName,
|
||||||
|
bizPackage, baseName,
|
||||||
|
author,
|
||||||
|
createDate,
|
||||||
|
createDate, author,
|
||||||
|
baseName, baseName,
|
||||||
|
baseName,
|
||||||
|
baseName,
|
||||||
|
baseName,
|
||||||
|
routingType, interfaceId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.writeString(usecaseImplDir.resolve(baseName + "UseCaseImpl.java"), serviceImplContent);
|
||||||
|
|
||||||
|
if (isMci) {
|
||||||
|
String mciReqContent = """
|
||||||
|
package %s.%s.io;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package %s.%s.io
|
||||||
|
* @className %s_I
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author %s
|
||||||
|
* @create %s
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class %s_I {
|
||||||
|
/**
|
||||||
|
* EAI 시스템이 요구하는 수신자 번호 파라미터명
|
||||||
|
*/
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EAI 시스템이 요구하는 메시지 내용 파라미터명
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
}
|
||||||
|
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
|
||||||
|
Files.writeString(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent);
|
||||||
|
|
||||||
|
String mciResContent = """
|
||||||
|
package %s.%s.io;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package %s.%s.io
|
||||||
|
* @className %s_O
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author %s
|
||||||
|
* @create %s
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class %s_O {
|
||||||
|
// TODO: Add response fields here
|
||||||
|
}
|
||||||
|
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
|
||||||
|
Files.writeString(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent);
|
||||||
|
|
||||||
|
String converterContent = """
|
||||||
|
package %s.converter;
|
||||||
|
|
||||||
|
import %s.dto.%sRequest;
|
||||||
|
import %s.dto.%sResponse;
|
||||||
|
import %s.%s.io.%s_I;
|
||||||
|
import %s.%s.io.%s_O;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
import org.mapstruct.factory.Mappers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package %s.converter
|
||||||
|
* @className %sConverter
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author %s
|
||||||
|
* @create %s
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Mapper(componentModel = "spring")
|
||||||
|
public interface %sConverter {
|
||||||
|
|
||||||
|
@Mapping(source = "phoneNumber", target = "phone")
|
||||||
|
@Mapping(source = "message", target = "content")
|
||||||
|
%s_I toLegacyRequest(%sRequest req);
|
||||||
|
|
||||||
|
@Mapping(source = "phone", target = "phoneNumber")
|
||||||
|
@Mapping(source = "content", target = "message")
|
||||||
|
%sRequest toRequest(%s_I mciReq);
|
||||||
|
|
||||||
|
// %sResponse toResponse(%s_O mciRes);
|
||||||
|
}
|
||||||
|
""".formatted(
|
||||||
|
bizPackage,
|
||||||
|
bizPackage, baseName,
|
||||||
|
bizPackage, baseName,
|
||||||
|
BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId,
|
||||||
|
BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId,
|
||||||
|
bizPackage, baseName, author, createDate, createDate, author,
|
||||||
|
baseName, interfaceId, baseName,
|
||||||
|
baseName, interfaceId,
|
||||||
|
baseName, interfaceId
|
||||||
|
);
|
||||||
|
Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent);
|
||||||
|
|
||||||
|
log.append("\n=========================================\n");
|
||||||
|
log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n");
|
||||||
|
log.append("=========================================\n");
|
||||||
|
log.append("[Usecase Interface] ").append(usecaseDir.resolve(baseName + "UseCase.java")).append("\n");
|
||||||
|
log.append("[Usecase Impl] ").append(usecaseImplDir.resolve(baseName + "UseCaseImpl.java")).append("\n");
|
||||||
|
log.append("[Request DTO] ").append(dtoDir.resolve(baseName + "Request.java")).append("\n");
|
||||||
|
log.append("[Response DTO] ").append(dtoDir.resolve(baseName + "Response.java")).append("\n");
|
||||||
|
log.append("[MCI Request IO] ").append(mciIoDir.resolve(interfaceId + "_I.java")).append("\n");
|
||||||
|
log.append("[MCI Response IO] ").append(mciIoDir.resolve(interfaceId + "_O.java")).append("\n");
|
||||||
|
log.append("[MCI Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n");
|
||||||
|
|
||||||
|
if (!clientPrefixCap.isEmpty()) {
|
||||||
|
String mciClientContent = """
|
||||||
|
package %s.%s;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent;
|
||||||
|
import io.shinhanlife.glow.communication.dto.Transfer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package %s.%s
|
||||||
|
* @className Mci%sClient
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author %s
|
||||||
|
* @create %s
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class Mci%sClient {
|
||||||
|
private final AxhubMciComponent mci;
|
||||||
|
|
||||||
|
public Transfer<Object> callTo(String interfaceId, String dummy, Object mciReq, Class<Object> resType) throws Exception {
|
||||||
|
return mci.callTo(interfaceId, dummy, mciReq, resType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".formatted(
|
||||||
|
BASE_PACKAGE, mciGroupPath.replace("/", "."),
|
||||||
|
BASE_PACKAGE, mciGroupPath.replace("/", "."), clientPrefixCap, author, createDate, createDate, author, clientPrefixCap
|
||||||
|
);
|
||||||
|
Files.writeString(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java"), mciClientContent);
|
||||||
|
log.append("[MCI Client] ").append(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java")).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
String legacyReqContent = """
|
||||||
|
package %s.legacy;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package %s.legacy
|
||||||
|
* @className %sLegacyRequest
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author %s
|
||||||
|
* @create %s
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class %sLegacyRequest {
|
||||||
|
/**
|
||||||
|
* EAI 시스템이 요구하는 수신자 번호 파라미터명
|
||||||
|
*/
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EAI 시스템이 요구하는 메시지 내용 파라미터명
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
}
|
||||||
|
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||||
|
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRequest.java"), legacyReqContent);
|
||||||
|
|
||||||
|
String legacyResContent = """
|
||||||
|
package %s.legacy;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package %s.legacy
|
||||||
|
* @className %sLegacyResponse
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author %s
|
||||||
|
* @create %s
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class %sLegacyResponse {
|
||||||
|
// TODO: Add legacy response fields here
|
||||||
|
}
|
||||||
|
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||||
|
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyResponse.java"), legacyResContent);
|
||||||
|
|
||||||
|
String converterContent = """
|
||||||
|
package %s.converter;
|
||||||
|
|
||||||
|
import %s.dto.%sRequest;
|
||||||
|
import %s.dto.%sResponse;
|
||||||
|
import %s.legacy.%sLegacyRequest;
|
||||||
|
import %s.legacy.%sLegacyResponse;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
import org.mapstruct.factory.Mappers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package %s.converter
|
||||||
|
* @className %sConverter
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author %s
|
||||||
|
* @create %s
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* %s %s 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Mapper(componentModel = "spring")
|
||||||
|
public interface %sConverter {
|
||||||
|
|
||||||
|
@Mapping(source = "phoneNumber", target = "phone")
|
||||||
|
@Mapping(source = "message", target = "content")
|
||||||
|
%sLegacyRequest toLegacyRequest(%sRequest req);
|
||||||
|
|
||||||
|
@Mapping(source = "phone", target = "phoneNumber")
|
||||||
|
@Mapping(source = "content", target = "message")
|
||||||
|
%sRequest toRequest(%sLegacyRequest legacyRequest);
|
||||||
|
|
||||||
|
// %sResponse toResponse(%sLegacyResponse legacyResponse);
|
||||||
|
}
|
||||||
|
""".formatted(
|
||||||
|
bizPackage,
|
||||||
|
bizPackage, baseName,
|
||||||
|
bizPackage, baseName,
|
||||||
|
bizPackage, baseName,
|
||||||
|
bizPackage, baseName,
|
||||||
|
bizPackage, baseName, author, createDate, createDate, author,
|
||||||
|
baseName, baseName, baseName, baseName, baseName, baseName, baseName
|
||||||
|
);
|
||||||
|
Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent);
|
||||||
|
|
||||||
|
log.append("\n=========================================\n");
|
||||||
|
log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n");
|
||||||
|
log.append("=========================================\n");
|
||||||
|
log.append("[Usecase Interface] ").append(usecaseDir.resolve(baseName + "UseCase.java")).append("\n");
|
||||||
|
log.append("[Usecase Impl] ").append(usecaseImplDir.resolve(baseName + "UseCaseImpl.java")).append("\n");
|
||||||
|
log.append("[Request DTO] ").append(dtoDir.resolve(baseName + "Request.java")).append("\n");
|
||||||
|
log.append("[Response DTO] ").append(dtoDir.resolve(baseName + "Response.java")).append("\n");
|
||||||
|
log.append("[Legacy Request DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyRequest.java")).append("\n");
|
||||||
|
log.append("[Legacy Response DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyResponse.java")).append("\n");
|
||||||
|
log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n");
|
||||||
|
}
|
||||||
|
// schema resource 파일 생성 (useSchemaResource=true 일 때)
|
||||||
|
if (useSchemaResource) {
|
||||||
|
Files.createDirectories(schemaDir);
|
||||||
|
String inputSchema = """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"TODO_FIELD": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "TODO: 파라미터 설명을 입력하세요."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
String outputSchema = """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "처리 결과 상태 (SUCCESS / FAILURE)",
|
||||||
|
"enum": ["SUCCESS", "FAILURE"]
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "처리 결과 메시지"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["status"]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Files.writeString(schemaDir.resolve(inputSchemaFileName), inputSchema);
|
||||||
|
Files.writeString(schemaDir.resolve(outputSchemaFileName), outputSchema);
|
||||||
|
log.append("[Input Schema] ").append(schemaDir.resolve(inputSchemaFileName)).append("\n");
|
||||||
|
log.append("[Output Schema] ").append(schemaDir.resolve(outputSchemaFileName)).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n");
|
||||||
|
|
||||||
|
return log.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toKebabCase(String pascalCase) {
|
||||||
|
if (pascalCase == null || pascalCase.isEmpty()) return pascalCase;
|
||||||
|
return pascalCase
|
||||||
|
.replaceAll("([a-z0-9])([A-Z])", "$1-$2")
|
||||||
|
.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toToolName(String moduleName, String group, String baseName) {
|
||||||
|
String moduleDirectory = Path.of(moduleName).getFileName().toString();
|
||||||
|
String pod = moduleDirectory.startsWith("dap-was-")
|
||||||
|
? moduleDirectory.substring("dap-was-".length())
|
||||||
|
: "oth";
|
||||||
|
String normalizedName = baseName.replaceAll("([a-z0-9])([A-Z])", "$1 $2")
|
||||||
|
.toLowerCase(Locale.ROOT)
|
||||||
|
.replaceAll("[^a-z0-9]+", " ")
|
||||||
|
.trim();
|
||||||
|
String[] words = normalizedName.split("\\s+");
|
||||||
|
String service = words[0];
|
||||||
|
String action = words.length == 1 ? "execute" : words[words.length - 1];
|
||||||
|
return "%s.%s.%s.%s".formatted(
|
||||||
|
pod.toLowerCase(Locale.ROOT),
|
||||||
|
group.toLowerCase(Locale.ROOT),
|
||||||
|
service,
|
||||||
|
action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toPascalCase(String str) {
|
||||||
|
if (str == null || str.isEmpty()) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
boolean capitalizeNext = true;
|
||||||
|
for (char c : str.toCharArray()) {
|
||||||
|
if (c == '_' || c == '-' || c == ' ') {
|
||||||
|
capitalizeNext = true;
|
||||||
|
} else if (capitalizeNext) {
|
||||||
|
result.append(Character.toUpperCase(c));
|
||||||
|
capitalizeNext = false;
|
||||||
|
} else {
|
||||||
|
result.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.length() > 0) {
|
||||||
|
result.setCharAt(0, Character.toUpperCase(result.charAt(0)));
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,14 @@
|
|||||||
package io.shinhanlife.dap.lib.util;
|
package io.shinhanlife.dap.lib.util;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.util
|
|
||||||
* @className ToolSchemaResolver
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
// removed McpOutputSchema
|
||||||
import io.shinhanlife.dap.lib.annotation.McpOutputSchema;import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
|
||||||
|
import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||||
|
|
||||||
/** Resolves an MCP Tool input schema from resource, inline value, or DTO metadata. */
|
/** Resolves an MCP Tool input schema from resource, inline value, or DTO metadata. */
|
||||||
public class ToolSchemaResolver {
|
public class ToolSchemaResolver {
|
||||||
|
|
||||||
@@ -32,13 +18,9 @@ public class ToolSchemaResolver {
|
|||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> resolve(McpFunction function, Class<?> requestType) {
|
public Map<String, Object> resolve(org.springframework.ai.mcp.annotation.McpTool function, ToolHint hint, Class<?> requestType) {
|
||||||
if (function != null && !function.inputSchemaResource().isBlank()) {
|
if (hint != null && !hint.inputSchemaResource().isBlank()) {
|
||||||
return loadResource(function.inputSchemaResource());
|
return loadResource(hint.inputSchemaResource());
|
||||||
}
|
|
||||||
if (function != null && !function.inputSchema().isBlank()
|
|
||||||
&& !"{}".equals(function.inputSchema().trim())) {
|
|
||||||
return parse(function.inputSchema(), "McpFunction.inputSchema");
|
|
||||||
}
|
}
|
||||||
return JsonSchemaGenerator.generateSchema(requestType);
|
return JsonSchemaGenerator.generateSchema(requestType);
|
||||||
}
|
}
|
||||||
@@ -46,25 +28,36 @@ public class ToolSchemaResolver {
|
|||||||
/**
|
/**
|
||||||
* Resolves an explicitly declared response schema.
|
* Resolves an explicitly declared response schema.
|
||||||
* Response schemas are opt-in so existing tools keep their current response behavior.
|
* Response schemas are opt-in so existing tools keep their current response behavior.
|
||||||
|
* ToolHint.outputSchemaResource()가 있으면 classpath JSON 파일에서 로드하고,
|
||||||
|
* 없으면 responseType DTO를 분석하여 자동 생성합니다.
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> resolveOutput(McpFunction function, Class<?> responseType) {
|
public Map<String, Object> resolveOutput(org.springframework.ai.mcp.annotation.McpTool function, Class<?> responseType, ToolHint hint) {
|
||||||
if (function != null && !function.outputSchemaResource().isBlank()) {
|
if (hint != null && !hint.outputSchemaResource().isBlank()) {
|
||||||
return loadResource(function.outputSchemaResource());
|
return loadResource(hint.outputSchemaResource());
|
||||||
}
|
}
|
||||||
if (function != null && !function.outputSchema().isBlank()
|
return resolveOutput(function, responseType);
|
||||||
&& !"{}".equals(function.outputSchema().trim())) {
|
}
|
||||||
return parse(function.outputSchema(), "McpFunction.outputSchema");
|
|
||||||
|
/**
|
||||||
|
* Resolves an explicitly declared response schema.
|
||||||
|
* Response schemas are opt-in so existing tools keep their current response behavior.
|
||||||
|
*/
|
||||||
|
public Map<String, Object> resolveOutput(org.springframework.ai.mcp.annotation.McpTool function, Class<?> responseType) {
|
||||||
|
// Object, Map 등 구체적인 DTO가 아닌 경우 검증 스킵
|
||||||
|
if (responseType == null
|
||||||
|
|| responseType == Object.class
|
||||||
|
|| Map.class.isAssignableFrom(responseType)
|
||||||
|
|| responseType == Void.class
|
||||||
|
|| responseType == void.class) {
|
||||||
|
return Map.of();
|
||||||
}
|
}
|
||||||
if (responseType != null && responseType.isAnnotationPresent(McpOutputSchema.class)) {
|
return JsonSchemaGenerator.generateSchema(responseType);
|
||||||
return JsonSchemaGenerator.generateSchema(responseType);
|
|
||||||
}
|
|
||||||
return Map.of();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retained for callers that use only explicit output schemas.
|
* Retained for callers that use only explicit output schemas.
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> resolveOutput(McpFunction function) {
|
public Map<String, Object> resolveOutput(org.springframework.ai.mcp.annotation.McpTool function) {
|
||||||
return resolveOutput(function, null);
|
return resolveOutput(function, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package io.shinhanlife.dap.lib.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
/** Updates MCP SDK and project-owned metadata in a generated tool source file. */
|
||||||
|
public final class ToolSourceUpdater {
|
||||||
|
|
||||||
|
private ToolSourceUpdater() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void updateToolSource(String toolName, String categoryKey, String description,
|
||||||
|
boolean register, Boolean requiresApproval) throws IOException {
|
||||||
|
String configuredSourceDirectory = System.getenv("AXHUB_SOURCE_DIR");
|
||||||
|
Path rootDirectory = configuredSourceDirectory == null || configuredSourceDirectory.isBlank()
|
||||||
|
? Paths.get(".") : Paths.get(configuredSourceDirectory);
|
||||||
|
updateToolSource(rootDirectory, toolName, categoryKey, description, register, requiresApproval);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void updateToolSource(Path rootDirectory, String toolName, String categoryKey, String description,
|
||||||
|
boolean register, Boolean requiresApproval) throws IOException {
|
||||||
|
Path targetFile = findToolSource(rootDirectory, toolName);
|
||||||
|
if (targetFile == null) {
|
||||||
|
throw new IllegalArgumentException("Tool source not found: " + toolName);
|
||||||
|
}
|
||||||
|
|
||||||
|
String content = Files.readString(targetFile);
|
||||||
|
content = updateMcpTool(content, toolName, description);
|
||||||
|
content = updateToolHint(content, categoryKey, register, requiresApproval);
|
||||||
|
Files.writeString(targetFile, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path findToolSource(Path rootDirectory, String toolName) throws IOException {
|
||||||
|
try (var paths = Files.walk(rootDirectory)) {
|
||||||
|
return paths.filter(Files::isRegularFile)
|
||||||
|
.filter(path -> path.getFileName().toString().endsWith("UseCase.java"))
|
||||||
|
.filter(path -> path.toString().contains("dap-was-") || path.toString().contains("axhub-tool-"))
|
||||||
|
.sorted(Comparator.naturalOrder())
|
||||||
|
.filter(path -> containsMcpTool(path, toolName))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean containsMcpTool(Path path, String toolName) {
|
||||||
|
try {
|
||||||
|
return annotationArguments(Files.readString(path), "McpTool", toolName) != null;
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new IllegalStateException("Failed to read tool source: " + path, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String updateMcpTool(String content, String toolName, String description) {
|
||||||
|
AnnotationRange range = annotationArguments(content, "McpTool", toolName);
|
||||||
|
if (range == null) {
|
||||||
|
throw new IllegalArgumentException("McpTool declaration not found: " + toolName);
|
||||||
|
}
|
||||||
|
return description == null ? content : replaceAttribute(content, range, "description", quote(description));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String updateToolHint(String content, String categoryKey, boolean register, Boolean requiresApproval) {
|
||||||
|
AnnotationRange range = annotationArguments(content, "ToolHint", null);
|
||||||
|
if (range == null) {
|
||||||
|
throw new IllegalArgumentException("ToolHint declaration not found next to McpTool");
|
||||||
|
}
|
||||||
|
String updated = replaceAttribute(content, range, "register", Boolean.toString(register));
|
||||||
|
range = annotationArguments(updated, "ToolHint", null);
|
||||||
|
if (requiresApproval != null) {
|
||||||
|
updated = replaceAttribute(updated, range, "requiresApproval", Boolean.toString(requiresApproval));
|
||||||
|
range = annotationArguments(updated, "ToolHint", null);
|
||||||
|
}
|
||||||
|
if (categoryKey != null && !categoryKey.isBlank()) {
|
||||||
|
updated = replaceAttribute(updated, range, "categoryKey", quote(categoryKey));
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String replaceAttribute(String content, AnnotationRange range, String attribute, String value) {
|
||||||
|
String arguments = content.substring(range.argumentsStart(), range.argumentsEnd());
|
||||||
|
String pattern = "\\b" + attribute + "\\s*=\\s*(?:true|false|\\\"(?:\\\\.|[^\\\"\\\\])*\\\")";
|
||||||
|
String replacement = arguments.replaceFirst(pattern, attribute + " = " + value);
|
||||||
|
if (replacement.equals(arguments)) {
|
||||||
|
replacement = arguments.isBlank() ? attribute + " = " + value : arguments + ", " + attribute + " = " + value;
|
||||||
|
}
|
||||||
|
return content.substring(0, range.argumentsStart()) + replacement + content.substring(range.argumentsEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String quote(String value) {
|
||||||
|
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnnotationRange annotationArguments(String content, String annotationName, String toolName) {
|
||||||
|
int offset = content.indexOf("@" + annotationName);
|
||||||
|
while (offset >= 0) {
|
||||||
|
int openingParenthesis = content.indexOf('(', offset);
|
||||||
|
int closingParenthesis = findAnnotationEnd(content, openingParenthesis);
|
||||||
|
if (openingParenthesis < 0 || closingParenthesis < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
AnnotationRange range = new AnnotationRange(openingParenthesis + 1, closingParenthesis);
|
||||||
|
if (toolName == null || content.substring(range.argumentsStart(), range.argumentsEnd())
|
||||||
|
.matches("(?s).*\\bname\\s*=\\s*\\\"" + java.util.regex.Pattern.quote(toolName) + "\\\".*")) {
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
offset = content.indexOf("@" + annotationName, closingParenthesis + 1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int findAnnotationEnd(String content, int openingParenthesis) {
|
||||||
|
int depth = 0;
|
||||||
|
boolean inString = false;
|
||||||
|
boolean escaped = false;
|
||||||
|
for (int index = openingParenthesis; index < content.length(); index++) {
|
||||||
|
char character = content.charAt(index);
|
||||||
|
if (inString) {
|
||||||
|
if (escaped) {
|
||||||
|
escaped = false;
|
||||||
|
} else if (character == '\\') {
|
||||||
|
escaped = true;
|
||||||
|
} else if (character == '\"') {
|
||||||
|
inString = false;
|
||||||
|
}
|
||||||
|
} else if (character == '\"') {
|
||||||
|
inString = true;
|
||||||
|
} else if (character == '(') {
|
||||||
|
depth++;
|
||||||
|
} else if (character == ')' && --depth == 0) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record AnnotationRange(int argumentsStart, int argumentsEnd) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.dap.lib.validation;
|
package io.shinhanlife.dap.lib.validation;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.validation
|
|
||||||
* @className McpToolNameValidationRunner
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
/** Gradle entry point for validating unique MCP Tool names before packaging. */
|
/** Gradle entry point for validating unique MCP Tool names before packaging. */
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.dap.lib.validation;
|
package io.shinhanlife.dap.lib.validation;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.validation
|
|
||||||
* @className McpToolNameValidator
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -31,7 +15,7 @@ import java.util.regex.Pattern;
|
|||||||
/**
|
/**
|
||||||
* @package io.shinhanlife.dap.lib.validation
|
* @package io.shinhanlife.dap.lib.validation
|
||||||
* @className McpToolNameValidator
|
* @className McpToolNameValidator
|
||||||
* @description Validates unique MCP function names across tool modules
|
* @description Validates unique MCP SDK tool names across tool modules
|
||||||
* @author 0986406
|
* @author 0986406
|
||||||
* @create 2026.07.27
|
* @create 2026.07.27
|
||||||
* <pre>
|
* <pre>
|
||||||
@@ -104,7 +88,7 @@ public final class McpToolNameValidator {
|
|||||||
throw new UncheckedIOException("Failed to read " + source, exception);
|
throw new UncheckedIOException("Failed to read " + source, exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
int annotationOffset = content.indexOf("@McpFunction");
|
int annotationOffset = content.indexOf("@McpTool");
|
||||||
while (annotationOffset >= 0) {
|
while (annotationOffset >= 0) {
|
||||||
int openingParenthesis = content.indexOf('(', annotationOffset);
|
int openingParenthesis = content.indexOf('(', annotationOffset);
|
||||||
int closingParenthesis = findAnnotationEnd(content, openingParenthesis);
|
int closingParenthesis = findAnnotationEnd(content, openingParenthesis);
|
||||||
@@ -119,7 +103,7 @@ public final class McpToolNameValidator {
|
|||||||
declarationsByName.computeIfAbsent(toolName, ignored -> new ArrayList<>())
|
declarationsByName.computeIfAbsent(toolName, ignored -> new ArrayList<>())
|
||||||
.add(new ToolDeclaration(moduleName, source, line));
|
.add(new ToolDeclaration(moduleName, source, line));
|
||||||
}
|
}
|
||||||
annotationOffset = content.indexOf("@McpFunction", closingParenthesis + 1);
|
annotationOffset = content.indexOf("@McpTool", closingParenthesis + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.dap.lib.validation;
|
package io.shinhanlife.dap.lib.validation;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.validation
|
|
||||||
* @className ToolArgumentSchemaValidator
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.networknt.schema.Error;
|
import com.networknt.schema.Error;
|
||||||
import com.networknt.schema.InputFormat;
|
import com.networknt.schema.InputFormat;
|
||||||
|
|||||||
@@ -2,14 +2,23 @@ package io.shinhanlife.dap.mcc.dto;
|
|||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import io.shinhanlife.dap.lib.dto.OperationType;
|
||||||
/**
|
/**
|
||||||
* @package io.shinhanlife.dap.mcc.dto
|
* Tool(Agent)의 명세 및 라우팅 정보를 담고 있는 메타데이터 클래스
|
||||||
|
* Redis 레지스트리에 저장되며, Planner와 Router 간의 통신 객체(Plan)로 사용됩니다.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcg.dto
|
||||||
* @className ToolMetadata
|
* @className ToolMetadata
|
||||||
* @description AX HUB 시스템 처리 클래스
|
* @description AX HUB 시스템 처리 클래스
|
||||||
* @author 0986406
|
* @author 0986406
|
||||||
@@ -22,17 +31,16 @@ import java.util.Map;
|
|||||||
*
|
*
|
||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
public class ToolMetadata {
|
public class ToolMetadata {
|
||||||
|
|
||||||
// 1. Tool 기본 정보
|
// 1. Tool 기본 정보
|
||||||
private String uid; // UUID 형식의 고유 식별자
|
private String uid; // UUID 형식의 고유 식별자
|
||||||
private String semver; // 버전 (예: 1.0.0)
|
private String semver; // 버전 (예: 1.0.0)
|
||||||
private Long timeoutMillis;
|
|
||||||
private Boolean enabled;
|
|
||||||
private String displayName; // 사람이 읽는 라벨 (1-128자)
|
private String displayName; // 사람이 읽는 라벨 (1-128자)
|
||||||
private String name; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool)
|
private String name; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool)
|
||||||
private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능)
|
private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능)
|
||||||
@@ -45,18 +53,26 @@ public class ToolMetadata {
|
|||||||
|
|
||||||
// 2-1. 도메인 부서 그룹명 (category_key, 슬러그 형식)
|
// 2-1. 도메인 부서 그룹명 (category_key, 슬러그 형식)
|
||||||
private String categoryKey;
|
private String categoryKey;
|
||||||
|
|
||||||
|
// 2-2. 툴 처리 엔드포인트 URI 경로 (예: /api/tool/customer-info)
|
||||||
private String endpoint;
|
private String endpoint;
|
||||||
|
|
||||||
|
// 2-3. Pod 실행 URL (독립적인 Microservice 라우팅용, 예: http://localhost:8082)
|
||||||
private String podUrl;
|
private String podUrl;
|
||||||
private String integrationType;
|
|
||||||
private String mciServiceId;
|
|
||||||
|
|
||||||
|
|
||||||
|
// 2-4. 가시성 여부
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private Boolean visible = true;
|
private Boolean visible = true;
|
||||||
|
|
||||||
|
// 활성화 여부
|
||||||
|
@Builder.Default
|
||||||
|
private Boolean enabled = true;
|
||||||
|
|
||||||
|
// 2-5. Redis 등록 여부 (UI 표출용)
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private Boolean isRegistered = true;
|
private Boolean isRegistered = true;
|
||||||
|
|
||||||
|
// 2-6. HITL 승인 필요 여부
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private Boolean requiresApproval = false;
|
private Boolean requiresApproval = false;
|
||||||
|
|
||||||
@@ -73,4 +89,45 @@ public class ToolMetadata {
|
|||||||
private Boolean openWorldHint = false;
|
private Boolean openWorldHint = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI)
|
||||||
|
private String integrationType; // 연동 타입: "DIRECT" 또는 "MCI_EAI"
|
||||||
|
|
||||||
|
// 4. 레거시(MCI/EAI) 연동 시 필수 정보 (integrationType이 "MCI_EAI"일 때 사용)
|
||||||
|
private String mciServiceId; // MCI/EAI 호출을 위한 서비스 ID (예: CRM_001, LICO_992)
|
||||||
|
|
||||||
|
// 5. 인프라 상태 정보 (DIRECT 연동 시 사용)
|
||||||
|
private Long lastHeartbeat; // Redis TTL 갱신용 마지막 하트비트 타임스탬프
|
||||||
|
|
||||||
|
// 6. 동적 서킷 브레이커 & 속도 제어 설정 (Registry 기반)
|
||||||
|
private Integer failureRateThreshold; // 서킷 브레이커 동작 기준 실패율 (%)
|
||||||
|
private Integer slidingWindowSize; // 서킷 브레이커 에러율 계산 표본 요청 수
|
||||||
|
private Integer rateLimitForPeriod; // 속도 제어: 1초당 허용 최대 요청 수
|
||||||
|
|
||||||
|
// 7. Gateway 코어 제어용 설정 필드 추가 (재시도, 타임아웃, 오퍼레이션 타입)
|
||||||
|
@Builder.Default
|
||||||
|
private OperationType operationType = OperationType.READ;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private Boolean retryEnabled = true;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private Integer circuitBreakerFailureThreshold = 0;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private Long circuitBreakerOpenMillis = 0L;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
private Long timeoutMillis = 0L;
|
||||||
|
|
||||||
|
// --- Guardrail 호환성을 위한 메서드 추가 ---
|
||||||
|
public Set<String> allowedArguments() {
|
||||||
|
if (parametersSchema == null || !parametersSchema.containsKey("properties")) return Set.of();
|
||||||
|
return ((Map<String, Object>) parametersSchema.get("properties")).keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> requiredArguments() {
|
||||||
|
if (parametersSchema == null || !parametersSchema.containsKey("required")) return Set.of();
|
||||||
|
return new HashSet<>((List<String>) parametersSchema.get("required"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,25 +4,24 @@ package io.shinhanlife.dap.mcc.presentation;
|
|||||||
/**
|
/**
|
||||||
* @package io.shinhanlife.dap.mcc.presentation
|
* @package io.shinhanlife.dap.mcc.presentation
|
||||||
* @className BusinessToolController
|
* @className BusinessToolController
|
||||||
* @description AX HUB ??戮?츩??嶺뚳퐣瑗????????
|
* @description AX HUB 시스템 처리 클래스
|
||||||
* @author 0986406
|
* @author 0986406
|
||||||
* @create 2026.09.01
|
* @create 2026.09.01
|
||||||
* <pre>
|
* <pre>
|
||||||
* ---------- ?띠룇裕??????----------
|
* ---------- 개정이력 ----------
|
||||||
* ??瑜곸젧?? ??瑜곸젧?? ??瑜곸젧??怨몃뮔
|
* 수정일 수정자 수정내용
|
||||||
* ---------- -------- ---------------------------
|
* ---------- -------- ---------------------------
|
||||||
* 2026.09.01 0986406 嶺뚣끉裕???諛댁뎽
|
* 2026.09.01 0986406 최초생성
|
||||||
*
|
*
|
||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.networknt.schema.Error;
|
import com.networknt.schema.Error;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpTool;
|
||||||
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpTool;
|
|
||||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||||
import io.shinhanlife.dap.lib.mcp.LocalToolScanner;
|
import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender;
|
||||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -55,17 +54,17 @@ public class BusinessToolController {
|
|||||||
private final ApplicationContext applicationContext;
|
private final ApplicationContext applicationContext;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final McpProperties mcpProperties;
|
private final McpProperties mcpProperties;
|
||||||
private final LocalToolScanner localToolScanner;
|
private final ToolRegistryHeartbeatSender toolRegistryHeartbeatSender;
|
||||||
private final ToolArgumentSchemaValidator toolArgumentSchemaValidator;
|
private final ToolArgumentSchemaValidator toolArgumentSchemaValidator;
|
||||||
|
|
||||||
private final ToolSchemaResolver toolSchemaResolver;
|
private final ToolSchemaResolver toolSchemaResolver;
|
||||||
// ???? ?브퀗?????β돦裕뉛쭚?Tool 嶺뚮ㅄ維뽨빳???븐뼔援?????
|
// 내부 조회용 로컬 Tool 목록 엔드포인트
|
||||||
@GetMapping("/mcp/api/v1/tools/local")
|
@GetMapping("/mcp/api/v1/tools/local")
|
||||||
public List<ToolMetadata> getLocalTools() {
|
public List<ToolMetadata> getLocalTools() {
|
||||||
return localToolScanner.getAllScannedTools();
|
return toolRegistryHeartbeatSender.getAllScannedTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ??戮?빢 REST ?リ옇?↑????됱쓤 ??源녿뮡????븐뼔援?????
|
// 순수 REST 기반 동적 라우팅 엔드포인트
|
||||||
@PostMapping("/mcp/{name}")
|
@PostMapping("/mcp/{name}")
|
||||||
public ResponseEntity<?> executeDynamicTool(
|
public ResponseEntity<?> executeDynamicTool(
|
||||||
@PathVariable("name") String functionName,
|
@PathVariable("name") String functionName,
|
||||||
@@ -78,25 +77,25 @@ public class BusinessToolController {
|
|||||||
String finalRequestId = headerRequestId;
|
String finalRequestId = headerRequestId;
|
||||||
|
|
||||||
log.info(" [Tool] IN - trace-id: {}, request-id: {}", traceId, requestId);
|
log.info(" [Tool] IN - trace-id: {}, request-id: {}", traceId, requestId);
|
||||||
log.info(" [Tool] ???됱쓤 ?????덈뺄 ??븐슙????琉용뼁 (??貫?양춯?: {}", functionName);
|
log.info(" [Tool] 동적 툴 실행 요청 수신 (함수명): {}", functionName);
|
||||||
if (arguments != null) {
|
if (arguments != null) {
|
||||||
try {
|
try {
|
||||||
log.info(" [Tool] ?筌뤾쑵?????逾ф쾬?롮구?? {}", objectMapper.writeValueAsString(arguments));
|
log.info(" [Tool] 호출 파라미터: {}", objectMapper.writeValueAsString(arguments));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.info(" [Tool] ?筌뤾쑵?????逾ф쾬?롮구?? {}", arguments);
|
log.info(" [Tool] 호출 파라미터: {}", arguments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Object targetBean = null;
|
Object targetBean = null;
|
||||||
Method targetMethod = null;
|
Method targetMethod = null;
|
||||||
McpFunction targetFunctionAnnotation = null;
|
McpTool targetFunctionAnnotation = null;
|
||||||
|
|
||||||
// McpTool ???????逾???リ옇?↑??브퀗???뱀쾸? ?熬곣뫁夷????쒖굣?節놁뿉??熬곣뫁逾???????깅さ亦껋깢??? ?熬곣뫕??????????щ뤂????@McpFunction??嶺뚢돦堉????덈펲.
|
// McpTool 어노테이션 기반 조회가 프록시 문제로 누락될 수 있으므로, 전체 빈을 순회하며 @McpTool을 찾습니다.
|
||||||
Map<String, Object> allBeans = applicationContext.getBeansOfType(Object.class);
|
Map<String, Object> allBeans = applicationContext.getBeansOfType(Object.class);
|
||||||
outerLoop:
|
outerLoop:
|
||||||
for (Object bean : allBeans.values()) {
|
for (Object bean : allBeans.values()) {
|
||||||
Class<?> targetClass = AopUtils.getTargetClass(bean);
|
Class<?> targetClass = AopUtils.getTargetClass(bean);
|
||||||
for (Method targetMethodOfClass : targetClass.getDeclaredMethods()) {
|
for (Method targetMethodOfClass : targetClass.getDeclaredMethods()) {
|
||||||
McpFunction mcpFunc = AnnotationUtils.findAnnotation(targetMethodOfClass, McpFunction.class);
|
McpTool mcpFunc = AnnotationUtils.findAnnotation(targetMethodOfClass, McpTool.class);
|
||||||
if (mcpFunc != null) {
|
if (mcpFunc != null) {
|
||||||
String baseName = mcpFunc.name();
|
String baseName = mcpFunc.name();
|
||||||
String expectedName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
String expectedName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
||||||
@@ -122,7 +121,7 @@ public class BusinessToolController {
|
|||||||
for (Object bean : allBeans.values()) {
|
for (Object bean : allBeans.values()) {
|
||||||
Class<?> targetCls = AopUtils.getTargetClass(bean);
|
Class<?> targetCls = AopUtils.getTargetClass(bean);
|
||||||
for (Method m : targetCls.getDeclaredMethods()) {
|
for (Method m : targetCls.getDeclaredMethods()) {
|
||||||
McpFunction func = AnnotationUtils.findAnnotation(m, McpFunction.class);
|
McpTool func = AnnotationUtils.findAnnotation(m, McpTool.class);
|
||||||
if (func != null) {
|
if (func != null) {
|
||||||
String baseName = func.name();
|
String baseName = func.name();
|
||||||
String expName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
String expName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
||||||
@@ -131,28 +130,29 @@ public class BusinessToolController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.error("[Tool] ???덈뺄????貫??Method)??嶺뚢돦堉??????怨룸????덈펲: {}. ?熬곣뫗?????노뼌????嶺뚮∥?꾥땻??嶺뚮ㅄ維뽨빳? {}", functionName, availableFunctions);
|
log.error("[Tool] 실행할 함수(Method)를 찾을 수 없습니다: {}. 현재 스캔된 툴 메서드 목록: {}", functionName, availableFunctions);
|
||||||
Map<String, Object> errorDetails = new HashMap<>();
|
Map<String, Object> errorDetails = new HashMap<>();
|
||||||
errorDetails.put("status", "404");
|
errorDetails.put("status", "404");
|
||||||
Map<String, Object> errorBody = new HashMap<>();
|
Map<String, Object> errorBody = new HashMap<>();
|
||||||
errorBody.put("code", "TOOL_NOT_FOUND");
|
errorBody.put("code", "TOOL_NOT_FOUND");
|
||||||
errorBody.put("message", "???덈뺄????貫???嶺뚢돦堉??????怨룸????덈펲: " + functionName);
|
errorBody.put("message", "실행할 함수를 찾을 수 없습니다: " + functionName);
|
||||||
errorBody.put("details", errorDetails);
|
errorBody.put("details", errorDetails);
|
||||||
if (finalRequestId != null) errorBody.put("request_id", finalRequestId);
|
if (finalRequestId != null) errorBody.put("request_id", finalRequestId);
|
||||||
|
|
||||||
return ResponseEntity.status(404).body(errorBody);
|
return ResponseEntity.status(404).body(errorBody);
|
||||||
}
|
}
|
||||||
// (?リ옇???嶺뚢뼰維???β돦裕뉐퐲???蹂ㅽ깴??
|
// (기존 차단 로직 제거됨)
|
||||||
|
|
||||||
// 2. ???逾ф쾬?롮구????ル쪇????롪틵?嶺?(JSON Schema)
|
// 2. 파라미터 유효성 검증 (JSON Schema)
|
||||||
if (targetMethod.getParameterCount() > 0) {
|
if (targetMethod.getParameterCount() > 0) {
|
||||||
Class<?> paramType = targetMethod.getParameterTypes()[0];
|
Class<?> paramType = targetMethod.getParameterTypes()[0];
|
||||||
if (!Map.class.isAssignableFrom(paramType)) {
|
if (!Map.class.isAssignableFrom(paramType)) {
|
||||||
try {
|
try {
|
||||||
Map<String, Object> inputSchema = toolSchemaResolver.resolve(targetFunctionAnnotation, paramType);
|
io.shinhanlife.dap.lib.annotation.ToolHint hint = AnnotationUtils.findAnnotation(targetMethod, io.shinhanlife.dap.lib.annotation.ToolHint.class);
|
||||||
|
Map<String, Object> inputSchema = toolSchemaResolver.resolve(targetFunctionAnnotation, hint, paramType);
|
||||||
List<Error> errors = toolArgumentSchemaValidator.validate(inputSchema, arguments);
|
List<Error> errors = toolArgumentSchemaValidator.validate(inputSchema, arguments);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
log.error("[Tool] ???逾ф쾬?롮구????ル쪇????롪틵?嶺????덉넮: {}", errors);
|
log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors);
|
||||||
List<String> errorMessages = new ArrayList<>();
|
List<String> errorMessages = new ArrayList<>();
|
||||||
for (Error validationError : errors) {
|
for (Error validationError : errors) {
|
||||||
errorMessages.add(validationError.getMessage());
|
errorMessages.add(validationError.getMessage());
|
||||||
@@ -161,28 +161,28 @@ public class BusinessToolController {
|
|||||||
errorDetails.put("status", "422");
|
errorDetails.put("status", "422");
|
||||||
Map<String, Object> errorBody = new HashMap<>();
|
Map<String, Object> errorBody = new HashMap<>();
|
||||||
errorBody.put("code", "INVALID_PARAM");
|
errorBody.put("code", "INVALID_PARAM");
|
||||||
errorBody.put("message", "???逾ф쾬?롮구????ル쪇????롪틵?嶺????덉넮");
|
errorBody.put("message", "파라미터 유효성 검증 실패");
|
||||||
errorBody.put("details", errorDetails);
|
errorBody.put("details", errorDetails);
|
||||||
if (finalRequestId != null) errorBody.put("request_id", finalRequestId);
|
if (finalRequestId != null) errorBody.put("request_id", finalRequestId);
|
||||||
|
|
||||||
return ResponseEntity.status(422).body(errorBody);
|
return ResponseEntity.status(422).body(errorBody);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[Tool] ???꾪뀞嶺??롪틵?嶺?繞????댁쾼 ?꾩룇裕뉑틦? {}", e.getMessage());
|
log.error("[Tool] 스키마 검증 중 오류 발생: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("[Tool] ?洹먮뿫???源끒?嶺뚯쉳??????덈뺄 -> Method: {}", targetMethod.getName());
|
log.info("[Tool] 리플렉션 직접 실행 -> Method: {}", targetMethod.getName());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 3. DTO ???逾ф쾬?롮구?????吏?嶺뚮씞?뗩뇡?(Map -> DTO)
|
// 3. DTO 파라미터 자동 매핑 (Map -> DTO)
|
||||||
Object invokeArgument = arguments;
|
Object invokeArgument = arguments;
|
||||||
if (targetMethod.getParameterCount() > 0 && arguments != null) {
|
if (targetMethod.getParameterCount() > 0 && arguments != null) {
|
||||||
Class<?> paramType = targetMethod.getParameterTypes()[0];
|
Class<?> paramType = targetMethod.getParameterTypes()[0];
|
||||||
if (!Map.class.isAssignableFrom(paramType)) {
|
if (!Map.class.isAssignableFrom(paramType)) {
|
||||||
invokeArgument = objectMapper.convertValue(arguments, paramType);
|
invokeArgument = objectMapper.convertValue(arguments, paramType);
|
||||||
log.info("[Tool] DTO ???吏?嶺뚮씞?뗩뇡??繹먭퍓沅? {}", paramType.getSimpleName());
|
log.info("[Tool] DTO 자동 매핑 성공: {}", paramType.getSimpleName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +194,8 @@ public class BusinessToolController {
|
|||||||
methodResult = targetMethod.invoke(targetBean, invokeArgument);
|
methodResult = targetMethod.invoke(targetBean, invokeArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(targetFunctionAnnotation, targetMethod.getReturnType());
|
io.shinhanlife.dap.lib.annotation.ToolHint outputHint = AnnotationUtils.findAnnotation(targetMethod, io.shinhanlife.dap.lib.annotation.ToolHint.class);
|
||||||
|
Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(targetFunctionAnnotation, targetMethod.getReturnType(), outputHint);
|
||||||
if (!outputSchema.isEmpty()) {
|
if (!outputSchema.isEmpty()) {
|
||||||
List<Error> outputErrors = toolArgumentSchemaValidator.validateValue(outputSchema, methodResult);
|
List<Error> outputErrors = toolArgumentSchemaValidator.validateValue(outputSchema, methodResult);
|
||||||
if (!outputErrors.isEmpty()) {
|
if (!outputErrors.isEmpty()) {
|
||||||
@@ -212,11 +213,11 @@ public class BusinessToolController {
|
|||||||
|
|
||||||
long elapsed = System.currentTimeMillis() - startTime;
|
long elapsed = System.currentTimeMillis() - startTime;
|
||||||
|
|
||||||
// 5. ?롪퍒????꾩룇瑗??(??戮?빢 REST ??얜Ŧ堉?
|
// 5. 결과 반환 (순수 REST 응답)
|
||||||
try {
|
try {
|
||||||
log.info("[Tool Execution] Output Schema Result: {}", objectMapper.writeValueAsString(methodResult));
|
log.info("[Tool -> MCP Gateway] Output Schema Result: {}", objectMapper.writeValueAsString(methodResult));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.info("[Tool Execution] Output Schema Result: {}", methodResult);
|
log.info("[Tool -> MCP Gateway] Output Schema Result: {}", methodResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(" [Tool] OUT - trace-id: {}, request-id: {}", traceId, requestId);
|
log.info(" [Tool] OUT - trace-id: {}, request-id: {}", traceId, requestId);
|
||||||
@@ -228,7 +229,7 @@ public class BusinessToolController {
|
|||||||
return responseBuilder.body(methodResult);
|
return responseBuilder.body(methodResult);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[Tool] ?洹먮뿫???源끒????덈뺄 繞????깅뇶 ?꾩룇裕뉑틦? {}", e.getMessage());
|
log.error("[Tool] 리플렉션 실행 중 예외 발생: {}", e.getMessage());
|
||||||
Map<String, Object> errorDetails = new HashMap<>();
|
Map<String, Object> errorDetails = new HashMap<>();
|
||||||
errorDetails.put("status", "500");
|
errorDetails.put("status", "500");
|
||||||
Map<String, Object> errorBody = new HashMap<>();
|
Map<String, Object> errorBody = new HashMap<>();
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.dap.mcc.presentation;
|
package io.shinhanlife.dap.mcc.presentation;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.mcc.presentation
|
|
||||||
* @className ToolManifestController
|
|
||||||
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 媛쒖젙?대젰 ----------
|
|
||||||
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 理쒖큹?앹꽦
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.manifest.ToolManifestResponse;
|
import io.shinhanlife.dap.lib.manifest.ToolManifestResponse;
|
||||||
import io.shinhanlife.dap.lib.manifest.ToolManifestService;
|
import io.shinhanlife.dap.lib.manifest.ToolManifestService;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
@@ -45,4 +29,4 @@ public class ToolManifestController {
|
|||||||
}
|
}
|
||||||
return ResponseEntity.ok().eTag(eTag).body(manifest);
|
return ResponseEntity.ok().eTag(eTag).body(manifest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.glow.communication.annotation;
|
package io.shinhanlife.glow.communication.annotation;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.glow.communication.annotation
|
|
||||||
* @className GlowTrgmField
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import java.lang.annotation.Documented;
|
import java.lang.annotation.Documented;
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
|
|||||||
@@ -1,20 +1,4 @@
|
|||||||
package io.shinhanlife.glow.communication.dto;
|
package io.shinhanlife.glow.communication.dto;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.glow.communication.dto
|
|
||||||
* @className CommonHeader
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@Data
|
@Data
|
||||||
public class CommonHeader {
|
public class CommonHeader {
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.glow.communication.dto;
|
package io.shinhanlife.glow.communication.dto;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.glow.communication.dto
|
|
||||||
* @className HeaderDefaults
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
public enum HeaderDefaults {
|
public enum HeaderDefaults {
|
||||||
ITRF_ID, RCV_SVC_ID, STR_YMD, ACNT_OGNZ_NO, PSMR_ASRT_CD, SBSN_RULP_ASRT_CD, BSDU_CD, BSQU_CD,
|
ITRF_ID, RCV_SVC_ID, STR_YMD, ACNT_OGNZ_NO, PSMR_ASRT_CD, SBSN_RULP_ASRT_CD, BSDU_CD, BSQU_CD,
|
||||||
INDV_CTIN_ROLE_CD, SCRN_ID, OGNZ_ASRT_CD, OGNZ_LEVE_CD, TGRM_CREA_CHNN_TYPE_CD, ENVR_TYPE_CD
|
INDV_CTIN_ROLE_CD, SCRN_ID, OGNZ_ASRT_CD, OGNZ_LEVE_CD, TGRM_CREA_CHNN_TYPE_CD, ENVR_TYPE_CD
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.glow.communication.dto;
|
package io.shinhanlife.glow.communication.dto;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.glow.communication.dto
|
|
||||||
* @className Transfer
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.glow.communication.exception;
|
package io.shinhanlife.glow.communication.exception;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.glow.communication.exception
|
|
||||||
* @className ItrfException
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
public class ItrfException extends Exception {
|
public class ItrfException extends Exception {
|
||||||
public ItrfException(String msg) {
|
public ItrfException(String msg) {
|
||||||
super(msg);
|
super(msg);
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.glow.communication.module.eai.component;
|
package io.shinhanlife.glow.communication.module.eai.component;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.glow.communication.module.eai.component
|
|
||||||
* @className GlowEaiComponent
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import io.shinhanlife.glow.communication.dto.Transfer;
|
import io.shinhanlife.glow.communication.dto.Transfer;
|
||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.glow.communication.module.mci.component;
|
package io.shinhanlife.glow.communication.module.mci.component;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.glow.communication.module.mci.component
|
|
||||||
* @className GlowMciComponent
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import io.shinhanlife.glow.communication.dto.Transfer;
|
import io.shinhanlife.glow.communication.dto.Transfer;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,4 @@
|
|||||||
package io.shinhanlife.glow.communication.util;
|
package io.shinhanlife.glow.communication.util;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.glow.communication.util
|
|
||||||
* @className CommonHeaderFactory
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
import io.shinhanlife.glow.communication.dto.CommonHeader;
|
import io.shinhanlife.glow.communication.dto.CommonHeader;
|
||||||
import io.shinhanlife.glow.communication.dto.HeaderDefaults;
|
import io.shinhanlife.glow.communication.dto.HeaderDefaults;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.glow.util;
|
package io.shinhanlife.glow.util;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.glow.util
|
|
||||||
* @className GlowMciParser
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import io.shinhanlife.glow.GlowMciFieldInfo;
|
import io.shinhanlife.glow.GlowMciFieldInfo;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ logging:
|
|||||||
pattern:
|
pattern:
|
||||||
console: "[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger{36} - %msg%n"
|
console: "[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger{36} - %msg%n"
|
||||||
|
|
||||||
# 2. Redis 공통 설정 (WAS 연동 및 캐싱)
|
# 2. Redis 공통 설정 (Gateway 연동 및 캐싱)
|
||||||
spring:
|
spring:
|
||||||
data:
|
data:
|
||||||
redis:
|
redis:
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<div class="flex items-center space-x-5">
|
<div class="flex items-center space-x-5">
|
||||||
<a href="/index.html" class="flex items-center group" style="text-decoration:none;">
|
<a href="/index.html" class="flex items-center group" style="text-decoration:none;">
|
||||||
<div class="w-2 h-2 rounded-full mr-2" style="background:#3b82f6; box-shadow: 0 0 8px rgba(59,130,246,0.8);"></div>
|
<div class="w-2 h-2 rounded-full mr-2" style="background:#3b82f6; box-shadow: 0 0 8px rgba(59,130,246,0.8);"></div>
|
||||||
<span class="font-semibold tracking-tight text-sm" style="color:#f4f4f5;">AXHUB WAS</span>
|
<span class="font-semibold tracking-tight text-sm" style="color:#f4f4f5;">AXHUB Gateway</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="h-4 w-px" style="background:#27272a;"></div>
|
<div class="h-4 w-px" style="background:#27272a;"></div>
|
||||||
<nav class="flex space-x-5 text-[13px] font-medium">
|
<nav class="flex space-x-5 text-[13px] font-medium">
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
function requestId() { return crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`; }
|
function requestId() { return crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`; }
|
||||||
function escapeHtml(value) { return String(value).replace(/[&<>]/g, item => ({'&':'&','<':'<','>':'>'}[item])); }
|
function escapeHtml(value) { return String(value).replace(/[&<>]/g, item => ({'&':'&','<':'<','>':'>'}[item])); }
|
||||||
|
|
||||||
let isWasMode = false;
|
let isGatewayMode = false;
|
||||||
|
|
||||||
async function loadManifest() {
|
async function loadManifest() {
|
||||||
$('manifestStatus').textContent = 'Manifest loading';
|
$('manifestStatus').textContent = 'Manifest loading';
|
||||||
@@ -90,13 +90,13 @@
|
|||||||
if (!response.ok && response.status === 404) {
|
if (!response.ok && response.status === 404) {
|
||||||
// Gateway 환경 감지 및 Fallback 처리
|
// Gateway 환경 감지 및 Fallback 처리
|
||||||
response = await fetch('/mcp/api/v1/tools/list');
|
response = await fetch('/mcp/api/v1/tools/list');
|
||||||
if (!response.ok) throw new Error(`WAS HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`Gateway HTTP ${response.status}`);
|
||||||
|
|
||||||
const rpcData = await response.json();
|
const rpcData = await response.json();
|
||||||
state.tools = rpcData.result?.tools || [];
|
state.tools = rpcData.result?.tools || [];
|
||||||
isWasMode = true;
|
isGatewayMode = true;
|
||||||
|
|
||||||
$('manifestStatus').textContent = `${state.tools.length} tools · WAS Mode`;
|
$('manifestStatus').textContent = `${state.tools.length} tools · Gateway Mode`;
|
||||||
$('manifestStatus').className = 'badge ok';
|
$('manifestStatus').className = 'badge ok';
|
||||||
renderTools();
|
renderTools();
|
||||||
return;
|
return;
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
$('executeButton').disabled = true; $('httpStatus').textContent = '실행 중'; $('httpStatus').className = 'badge';
|
$('executeButton').disabled = true; $('httpStatus').textContent = '실행 중'; $('httpStatus').className = 'badge';
|
||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
if (isWasMode) {
|
if (isGatewayMode) {
|
||||||
const reqPayload = { jsonrpc: "2.0", method: "tools/call", params: { name: tool.name, arguments: payload }, id: Date.now() };
|
const reqPayload = { jsonrpc: "2.0", method: "tools/call", params: { name: tool.name, arguments: payload }, id: Date.now() };
|
||||||
response = await fetch('/mcp/api/v1/tools/call', {
|
response = await fetch('/mcp/api/v1/tools/call', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
|
|
||||||
let isOk = response.ok;
|
let isOk = response.ok;
|
||||||
let displayStatus = response.status;
|
let displayStatus = response.status;
|
||||||
if (isWasMode && isOk && data?.result?.isError) {
|
if (isGatewayMode && isOk && data?.result?.isError) {
|
||||||
isOk = false;
|
isOk = false;
|
||||||
displayStatus = "MCP ERR";
|
displayStatus = "MCP ERR";
|
||||||
}
|
}
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
$('httpStatus').textContent = `HTTP ${displayStatus}`; $('httpStatus').className = `badge ${isOk ? 'ok' : 'fail'}`;
|
$('httpStatus').textContent = `HTTP ${displayStatus}`; $('httpStatus').className = `badge ${isOk ? 'ok' : 'fail'}`;
|
||||||
$('latency').textContent = `${elapsed}ms`; $('traceId').textContent = `trace-id: ${response.headers.get('trace-id') || trace}`; $('requestId').textContent = `request-id: ${response.headers.get('request-id') || request}`;
|
$('latency').textContent = `${elapsed}ms`; $('traceId').textContent = `trace-id: ${response.headers.get('trace-id') || trace}`; $('requestId').textContent = `request-id: ${response.headers.get('request-id') || request}`;
|
||||||
let displayData = data;
|
let displayData = data;
|
||||||
if (isWasMode && data && typeof data === 'object') {
|
if (isGatewayMode && data && typeof data === 'object') {
|
||||||
if (data.result && data.result.result) {
|
if (data.result && data.result.result) {
|
||||||
displayData = data.result.result.data !== undefined ? data.result.result.data : data.result.result;
|
displayData = data.result.result.data !== undefined ? data.result.result.data : data.result.result;
|
||||||
} else if (data.error) {
|
} else if (data.error) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package io.shinhanlife.dap.lib.adapter.sender;
|
package io.shinhanlife.dap.lib.common.adapter.sender;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.shinhanlife.dap.lib.integration.mci.dto.MciRequestWrapper;
|
import io.shinhanlife.dap.lib.integration.mci.dto.MciRequestWrapper;
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.dap.lib.config;
|
package io.shinhanlife.dap.lib.config;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.config
|
|
||||||
* @className ToolSchemaConfigurationTest
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|||||||
@@ -1,21 +1,7 @@
|
|||||||
package io.shinhanlife.dap.lib.util;
|
package io.shinhanlife.dap.lib.util;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.util
|
|
||||||
* @className JsonSchemaGeneratorTest
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@@ -26,8 +12,6 @@ import com.networknt.schema.InputFormat;
|
|||||||
import com.networknt.schema.Schema;
|
import com.networknt.schema.Schema;
|
||||||
import com.networknt.schema.SchemaRegistry;
|
import com.networknt.schema.SchemaRegistry;
|
||||||
import com.networknt.schema.SpecificationVersion;
|
import com.networknt.schema.SpecificationVersion;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -106,24 +90,19 @@ class JsonSchemaGeneratorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class ValidatedRequest {
|
private static class ValidatedRequest {
|
||||||
@McpParameter(description = "recipient phone number", required = true)
|
@McpToolParam(description = "recipient phone number", required = true)
|
||||||
@McpValidation(pattern = "^01[0-9]{8,9}$")
|
|
||||||
private String phoneNumber;
|
private String phoneNumber;
|
||||||
|
|
||||||
@McpParameter(description = "issue amount", required = true)
|
@McpToolParam(description = "issue amount", required = true)
|
||||||
@McpValidation(minimum = 1)
|
|
||||||
private Long amount;
|
private Long amount;
|
||||||
|
|
||||||
@McpParameter(description = "approval result")
|
@McpToolParam(description = "approval result")
|
||||||
@McpValidation(required = true, allowedValues = {"APPROVE", "REJECT"})
|
|
||||||
private String approvalStatus;
|
private String approvalStatus;
|
||||||
|
|
||||||
@McpParameter(description = "page size")
|
@McpToolParam(description = "page size")
|
||||||
@McpValidation(minimum = 1, maximum = 50, defaultValue = "20")
|
|
||||||
private Integer pageSize;
|
private Integer pageSize;
|
||||||
|
|
||||||
@McpParameter(description = "reference")
|
@McpToolParam(description = "reference")
|
||||||
@McpValidation(minLength = 1, maxLength = 30)
|
|
||||||
private String reference;
|
private String reference;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +115,6 @@ class JsonSchemaGeneratorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class NestedChild {
|
private static class NestedChild {
|
||||||
@McpValidation(required = true, pattern = "^\\d{8}$")
|
|
||||||
private String businessDate;
|
private String businessDate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package io.shinhanlife.dap.lib.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class ToolScaffolderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void generatesManifestReadyToolAndOutputSchemaDto() throws Exception {
|
||||||
|
String moduleName = "build/scaffold-manifest-test";
|
||||||
|
|
||||||
|
ToolScaffolder.scaffold("claim search", "CLM0001", "청구 조회", "cmm", "HTTP", moduleName,
|
||||||
|
"tester", "2026.08.04", true, null);
|
||||||
|
|
||||||
|
Path root = Path.of(moduleName, "src/main/java/io/shinhanlife/dap/mcc/biz/cmm");
|
||||||
|
String useCase = Files.readString(root.resolve("usecase/ClaimSearchUseCase.java"));
|
||||||
|
String response = Files.readString(root.resolve("dto/ClaimSearchResponse.java"));
|
||||||
|
|
||||||
|
assertTrue(useCase.contains("name = \"oth.cmm.claim.search\""));
|
||||||
|
assertTrue(useCase.contains("version = \"1.0.0\""));
|
||||||
|
assertTrue(useCase.contains("timeoutMillis = 300000L"));
|
||||||
|
assertTrue(response.contains(""));
|
||||||
|
assertTrue(response.contains(""));
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
void usesWasModuleNameAsToolPodPrefix() throws Exception {
|
||||||
|
String moduleName = "build/dap-was-sms";
|
||||||
|
|
||||||
|
ToolScaffolder.scaffold("notification send", "SMS0001", "SMS 발송", "cmm", "HTTP", moduleName,
|
||||||
|
"tester", "2026.08.05", true, null);
|
||||||
|
|
||||||
|
Path useCasePath = Path.of(moduleName,
|
||||||
|
"src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/NotificationSendUseCase.java");
|
||||||
|
String useCase = Files.readString(useCasePath);
|
||||||
|
|
||||||
|
assertTrue(useCase.contains("name = \"sms.cmm.notification.send\""));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +1,24 @@
|
|||||||
package io.shinhanlife.dap.lib.util;
|
package io.shinhanlife.dap.lib.util;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.util
|
|
||||||
* @className ToolSchemaResolverTest
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpOutputSchema;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpTool;
|
||||||
|
|
||||||
class ToolSchemaResolverTest {
|
class ToolSchemaResolverTest {
|
||||||
|
|
||||||
private final ToolSchemaResolver resolver = new ToolSchemaResolver(new ObjectMapper());
|
private final ToolSchemaResolver resolver = new ToolSchemaResolver(new ObjectMapper());
|
||||||
|
|
||||||
@Test
|
|
||||||
void usesInlineSchemaBeforeAutomaticDtoSchema() throws Exception {
|
|
||||||
Method method = InlineSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
|
||||||
|
|
||||||
Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutomaticRequest.class);
|
|
||||||
|
|
||||||
assertEquals(false, schema.get("additionalProperties"));
|
|
||||||
assertTrue(!properties(schema).containsKey("differentField"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void generatesSchemaFromRequestDtoWhenNoExplicitSchemaIsConfigured() throws Exception {
|
void generatesSchemaFromRequestDtoWhenNoExplicitSchemaIsConfigured() throws Exception {
|
||||||
Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
||||||
|
|
||||||
Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutomaticRequest.class);
|
Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpTool.class), null, AutomaticRequest.class);
|
||||||
|
|
||||||
assertTrue(properties(schema).containsKey("differentField"));
|
assertTrue(properties(schema).containsKey("differentField"));
|
||||||
}
|
}
|
||||||
@@ -54,7 +26,7 @@ class ToolSchemaResolverTest {
|
|||||||
@Test
|
@Test
|
||||||
void resolvesExplicitOutputSchema() throws Exception {
|
void resolvesExplicitOutputSchema() throws Exception {
|
||||||
Method method = OutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
Method method = OutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
||||||
Map<String, Object> schema = resolver.resolveOutput(method.getAnnotation(McpFunction.class));
|
Map<String, Object> schema = resolver.resolveOutput(method.getAnnotation(McpTool.class));
|
||||||
assertEquals(false, schema.get("additionalProperties"));
|
assertEquals(false, schema.get("additionalProperties"));
|
||||||
assertTrue(properties(schema).containsKey("resultCode"));
|
assertTrue(properties(schema).containsKey("resultCode"));
|
||||||
}
|
}
|
||||||
@@ -64,7 +36,7 @@ class ToolSchemaResolverTest {
|
|||||||
Method method = AutomaticOutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
Method method = AutomaticOutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
||||||
|
|
||||||
Map<String, Object> schema = resolver.resolveOutput(
|
Map<String, Object> schema = resolver.resolveOutput(
|
||||||
method.getAnnotation(McpFunction.class), SimpleResponse.class);
|
method.getAnnotation(McpTool.class), SimpleResponse.class);
|
||||||
|
|
||||||
assertEquals(List.of("resultCode"), schema.get("required"));
|
assertEquals(List.of("resultCode"), schema.get("required"));
|
||||||
assertEquals(List.of("SUCCESS", "FAILURE"), property(schema, "resultCode").get("enum"));
|
assertEquals(List.of("SUCCESS", "FAILURE"), property(schema, "resultCode").get("enum"));
|
||||||
@@ -74,7 +46,7 @@ class ToolSchemaResolverTest {
|
|||||||
void doesNotEnableOutputValidationWhenOutputSchemaIsNotDeclared() throws Exception {
|
void doesNotEnableOutputValidationWhenOutputSchemaIsNotDeclared() throws Exception {
|
||||||
Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
||||||
Map<String, Object> schema = resolver.resolveOutput(
|
Map<String, Object> schema = resolver.resolveOutput(
|
||||||
method.getAnnotation(McpFunction.class), AutomaticRequest.class);
|
method.getAnnotation(McpTool.class), AutomaticRequest.class);
|
||||||
assertTrue(schema.isEmpty());
|
assertTrue(schema.isEmpty());
|
||||||
}
|
}
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@@ -87,44 +59,27 @@ class ToolSchemaResolverTest {
|
|||||||
return (Map<String, Object>) properties(schema).get(name);
|
return (Map<String, Object>) properties(schema).get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
static class InlineSchemaTool {
|
|
||||||
@McpFunction(
|
|
||||||
displayName = "inline",
|
|
||||||
name = "test.schema.inline",
|
|
||||||
description = "inline schema",
|
|
||||||
inputSchema = "{\"type\":\"object\",\"properties\":{\"keyword\":{\"type\":\"string\"}},\"additionalProperties\":false}")
|
|
||||||
void search(AutomaticRequest request) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class AutomaticSchemaTool {
|
static class AutomaticSchemaTool {
|
||||||
@McpFunction(displayName = "automatic", name = "test.schema.automatic", description = "automatic schema")
|
@McpTool(name = "oth.test.automatic.search")
|
||||||
void search(AutomaticRequest request) {
|
void search(AutomaticRequest request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class AutomaticOutputSchemaTool {
|
static class AutomaticOutputSchemaTool {
|
||||||
@McpFunction(displayName = "automatic-output", name = "test.schema.automatic-output", description = "automatic output")
|
@McpTool(name = "oth.test.output.search")
|
||||||
SimpleResponse search(AutomaticRequest request) {
|
SimpleResponse search(AutomaticRequest request) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@McpOutputSchema
|
|
||||||
static class SimpleResponse {
|
static class SimpleResponse {
|
||||||
@McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"})
|
|
||||||
private String resultCode;
|
private String resultCode;
|
||||||
|
|
||||||
@McpValidation(maxLength = 200)
|
|
||||||
private String message;
|
private String message;
|
||||||
}
|
}
|
||||||
|
|
||||||
static class OutputSchemaTool {
|
static class OutputSchemaTool {
|
||||||
@McpFunction(
|
@McpTool(name = "oth.test.explicit.search")
|
||||||
displayName = "output",
|
|
||||||
name = "test.schema.output",
|
|
||||||
description = "output schema",
|
|
||||||
outputSchema = "{\"type\":\"object\",\"properties\":{\"resultCode\":{\"type\":\"string\"}},\"required\":[\"resultCode\"],\"additionalProperties\":false}")
|
|
||||||
void search(AutomaticRequest request) {
|
void search(AutomaticRequest request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package io.shinhanlife.dap.lib.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
class ToolSourceUpdaterTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path temporaryRoot;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updatesSdkAndProjectOwnedMetadataOnTheSameToolMethod() throws Exception {
|
||||||
|
Path source = temporaryRoot.resolve("dap-was-oth/src/main/java/example/SampleUseCase.java");
|
||||||
|
Files.createDirectories(source.getParent());
|
||||||
|
Files.writeString(source, """
|
||||||
|
package example;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpTool;
|
||||||
|
import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||||
|
interface SampleUseCase {
|
||||||
|
@McpTool(name = "oth.cmm.sample.search", description = "old")
|
||||||
|
@ToolHint(register = false, requiresApproval = false)
|
||||||
|
void search();
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
ToolSourceUpdater.updateToolSource(temporaryRoot, "oth.cmm.sample.search", "customer", "new", true, true);
|
||||||
|
|
||||||
|
String updated = Files.readString(source);
|
||||||
|
assertTrue(updated.contains("@McpTool(name = \"oth.cmm.sample.search\", description = \"new\")"));
|
||||||
|
assertTrue(updated.contains("@ToolHint(register = true, requiresApproval = true"));
|
||||||
|
assertTrue(updated.contains("categoryKey = \"customer\""), updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.dap.lib.validation;
|
package io.shinhanlife.dap.lib.validation;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.validation
|
|
||||||
* @className McpToolNameValidatorTest
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
@@ -46,7 +30,7 @@ class McpToolNameValidatorTest {
|
|||||||
Path temporaryRoot;
|
Path temporaryRoot;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsDuplicateMcpFunctionNamesAcrossToolModules() throws IOException {
|
void rejectsDuplicateMcpToolNamesAcrossToolModules() throws IOException {
|
||||||
writeToolSource("dap-was-first", "FirstTool.java", "first", "oth.sms.notification.send");
|
writeToolSource("dap-was-first", "FirstTool.java", "first", "oth.sms.notification.send");
|
||||||
writeToolSource("dap-was-second", "SecondTool.java", "second", "oth.sms.notification.send");
|
writeToolSource("dap-was-second", "SecondTool.java", "second", "oth.sms.notification.send");
|
||||||
|
|
||||||
@@ -59,7 +43,7 @@ class McpToolNameValidatorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void validationRunnerRejectsDuplicateMcpFunctionNamesBeforePackaging() throws IOException {
|
void validationRunnerRejectsDuplicateMcpToolNamesBeforePackaging() throws IOException {
|
||||||
writeToolSource("dap-was-first", "FirstTool.java", "first", "oth.sms.notification.send");
|
writeToolSource("dap-was-first", "FirstTool.java", "first", "oth.sms.notification.send");
|
||||||
writeToolSource("dap-was-second", "SecondTool.java", "second", "oth.sms.notification.send");
|
writeToolSource("dap-was-second", "SecondTool.java", "second", "oth.sms.notification.send");
|
||||||
|
|
||||||
@@ -106,27 +90,13 @@ class McpToolNameValidatorTest {
|
|||||||
Files.writeString(source, """
|
Files.writeString(source, """
|
||||||
package example;
|
package example;
|
||||||
|
|
||||||
/**
|
import org.springframework.ai.mcp.annotation.McpTool;
|
||||||
* @package io.shinhanlife.dap.lib.validation
|
|
||||||
* @className McpToolNameValidatorTest
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
class %s {
|
class %s {
|
||||||
@McpFunction(displayName = "%s", name = "%s", description = "test")
|
@McpTool(name = "%s")
|
||||||
void execute() { }
|
void execute() { }
|
||||||
}
|
}
|
||||||
""".formatted(className, className, toolName));
|
""".formatted(className, toolName));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path findProjectRoot() {
|
private Path findProjectRoot() {
|
||||||
|
|||||||
@@ -1,27 +1,14 @@
|
|||||||
package io.shinhanlife.dap.lib.manifest;
|
package io.shinhanlife.dap.lib.manifest;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.manifest
|
|
||||||
* @className ToolManifestServiceTest
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||||
|
import io.shinhanlife.dap.lib.manifest.ToolManifestItem;
|
||||||
|
import io.shinhanlife.dap.lib.manifest.ToolManifestResponse;
|
||||||
|
import io.shinhanlife.dap.lib.manifest.ToolManifestService;
|
||||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -1,20 +1,4 @@
|
|||||||
package io.shinhanlife.dap.lib.mcp;
|
package io.shinhanlife.dap.mcc.mcp;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.mcp
|
|
||||||
* @className McpRequestHeaderFilterTest
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
@@ -26,8 +10,9 @@ import static org.mockito.Mockito.when;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.modelcontextprotocol.server.McpSyncServer;
|
import io.modelcontextprotocol.server.McpSyncServer;
|
||||||
import io.shinhanlife.dap.lib.presentation.BusinessToolController;
|
import io.shinhanlife.dap.lib.mcp.*;
|
||||||
import io.shinhanlife.dap.lib.mcp.LocalToolScanner;
|
import io.shinhanlife.dap.mcc.presentation.BusinessToolController;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -40,14 +25,14 @@ class McpRequestHeaderFilterTest {
|
|||||||
@Test
|
@Test
|
||||||
void capturesOptionalMcpHeadersOnlyForTheCurrentRequest() throws Exception {
|
void capturesOptionalMcpHeadersOnlyForTheCurrentRequest() throws Exception {
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/mcp");
|
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/mcp");
|
||||||
request.addHeader("X-Request-Id", "was-request-id");
|
request.addHeader("X-Request-Id", "gateway-request-id");
|
||||||
request.addHeader("trace-id", "trace-001");
|
request.addHeader("trace-id", "trace-001");
|
||||||
request.addHeader("request-id", "tool-request-001");
|
request.addHeader("request-id", "tool-request-001");
|
||||||
request.addHeader("employee-id", "encrypted-employee-id");
|
request.addHeader("employee-id", "encrypted-employee-id");
|
||||||
|
|
||||||
new McpRequestHeaderFilter().doFilter(request, new MockHttpServletResponse(), (req, res) -> {
|
new McpRequestHeaderFilter().doFilter(request, new MockHttpServletResponse(), (req, res) -> {
|
||||||
McpRequestHeaders headers = McpRequestHeaderContext.current();
|
McpRequestHeaders headers = McpRequestHeaderContext.current();
|
||||||
assertEquals("was-request-id", headers.headerRequestId());
|
assertEquals("gateway-request-id", headers.headerRequestId());
|
||||||
assertEquals("trace-001", headers.traceId());
|
assertEquals("trace-001", headers.traceId());
|
||||||
assertEquals("tool-request-001", headers.requestId());
|
assertEquals("tool-request-001", headers.requestId());
|
||||||
assertEquals("encrypted-employee-id", headers.encryptedEmployeeId());
|
assertEquals("encrypted-employee-id", headers.encryptedEmployeeId());
|
||||||
@@ -60,19 +45,19 @@ class McpRequestHeaderFilterTest {
|
|||||||
void forwardsCapturedHeadersToBusinessToolExecution() throws Exception {
|
void forwardsCapturedHeadersToBusinessToolExecution() throws Exception {
|
||||||
BusinessToolController controller = mock(BusinessToolController.class);
|
BusinessToolController controller = mock(BusinessToolController.class);
|
||||||
doReturn(ResponseEntity.ok(Map.of("result", "ok")))
|
doReturn(ResponseEntity.ok(Map.of("result", "ok")))
|
||||||
.when(controller).executeDynamicTool(eq("sampleTool"), eq("was-request-id"), eq("trace-001"),
|
.when(controller).executeDynamicTool(eq("sampleTool"), eq("gateway-request-id"), eq("trace-001"),
|
||||||
eq("tool-request-001"), eq("encrypted-employee-id"), eq(Map.of("key", "value")));
|
eq("tool-request-001"), eq("encrypted-employee-id"), eq(Map.of("key", "value")));
|
||||||
ToolPodMcpToolSynchronizer synchronizer = new ToolPodMcpToolSynchronizer(
|
ToolPodMcpToolSynchronizer synchronizer = new ToolPodMcpToolSynchronizer(
|
||||||
mock(McpSyncServer.class), mock(LocalToolScanner.class), controller, new ObjectMapper());
|
mock(McpSyncServer.class), mock(ToolRegistryHeartbeatSender.class), controller, new ObjectMapper());
|
||||||
|
|
||||||
Method invoke = ToolPodMcpToolSynchronizer.class.getDeclaredMethod(
|
Method invoke = ToolPodMcpToolSynchronizer.class.getDeclaredMethod(
|
||||||
"invoke", String.class, McpRequestHeaders.class, Map.class);
|
"invoke", String.class, McpRequestHeaders.class, Map.class);
|
||||||
invoke.setAccessible(true);
|
invoke.setAccessible(true);
|
||||||
invoke.invoke(synchronizer, "sampleTool",
|
invoke.invoke(synchronizer, "sampleTool",
|
||||||
new McpRequestHeaders("was-request-id", "trace-001", "tool-request-001", "encrypted-employee-id"),
|
new McpRequestHeaders("gateway-request-id", "trace-001", "tool-request-001", "encrypted-employee-id"),
|
||||||
Map.of("key", "value"));
|
Map.of("key", "value"));
|
||||||
|
|
||||||
verify(controller).executeDynamicTool("sampleTool", "was-request-id", "trace-001",
|
verify(controller).executeDynamicTool("sampleTool", "gateway-request-id", "trace-001",
|
||||||
"tool-request-001", "encrypted-employee-id", Map.of("key", "value"));
|
"tool-request-001", "encrypted-employee-id", Map.of("key", "value"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,11 @@
|
|||||||
package io.shinhanlife.dap.lib.mcp;
|
package io.shinhanlife.dap.mcc.mcp;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.mcp
|
|
||||||
* @className ToolMcpServerConfigurationTest
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
|
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
|
||||||
|
import io.shinhanlife.dap.lib.mcp.ToolMcpServerConfiguration;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||||
|
|
||||||
@@ -32,7 +18,6 @@ class ToolMcpServerConfigurationTest {
|
|||||||
HttpServletStreamableServerTransportProvider transport = configuration.toolMcpTransportProvider();
|
HttpServletStreamableServerTransportProvider transport = configuration.toolMcpTransportProvider();
|
||||||
ServletRegistrationBean<HttpServletStreamableServerTransportProvider> registration = configuration.toolMcpServlet(transport);
|
ServletRegistrationBean<HttpServletStreamableServerTransportProvider> registration = configuration.toolMcpServlet(transport);
|
||||||
|
|
||||||
assertEquals("/mcp", registration.getUrlMappings().iterator().next());
|
assertThat(registration.getUrlMappings()).containsExactlyInAnyOrder("/mcp", "/mcp/message");
|
||||||
assertFalse(registration.getUrlMappings().contains("/mcp/*"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,4 @@
|
|||||||
package io.shinhanlife.dap.lib.presentation;
|
package io.shinhanlife.dap.mcc.presentation;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.presentation
|
|
||||||
* @className BusinessToolControllerHeaderContractTest
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
|||||||
@@ -1,25 +1,10 @@
|
|||||||
package io.shinhanlife.dap.lib.validation;
|
package io.shinhanlife.dap.mcc.presentation;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.validation
|
|
||||||
* @className ToolArgumentSchemaValidatorTest
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -1,20 +1,4 @@
|
|||||||
package io.shinhanlife.dap.lib.presentation;
|
package io.shinhanlife.dap.mcc.presentation;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.lib.presentation
|
|
||||||
* @className ToolTestConsoleResourceTest
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.glow.communication.annotation;
|
package io.shinhanlife.glow.communication.annotation;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.glow.communication.annotation
|
|
||||||
* @className GlowTrgmFieldContractTest
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
/**
|
/**
|
||||||
* @package io.shinhanlife.dap.mcc.dto
|
* @package io.shinhanlife.dap.mcc.dto
|
||||||
* @className BalanceRequest
|
* @className BalanceRequest
|
||||||
@@ -15,13 +20,12 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
|||||||
*
|
*
|
||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class BalanceRequest {
|
public class BalanceRequest {
|
||||||
@McpParameter(description = "고객의 계좌번호 (- 제외) ", required = true)
|
@McpToolParam(description = "고객의 계좌번호 (- 제외) ", required = true)
|
||||||
@McpValidation(pattern = "\\S")
|
@Schema(pattern = "\\S", example = "1234567890")
|
||||||
private String accountNumber;
|
private String accountNumber;
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@@ -30,11 +33,12 @@ import lombok.Setter;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class BillingProcessRequest {
|
public class BillingProcessRequest {
|
||||||
|
|
||||||
@McpParameter(description = "처리할 청구 접수 번호", required = true)
|
@McpToolParam(description = "처리할 청구 접수 번호", required = true)
|
||||||
@McpValidation(pattern = "\\S")
|
@Schema(pattern = "\\S", example = "BILL20260805")
|
||||||
private String billingId;
|
private String billingId;
|
||||||
|
|
||||||
@McpParameter(description = "심사 승인 여부 (예: APPROVE, REJECT)")
|
|
||||||
@McpValidation(required = true, allowedValues = {"APPROVE", "REJECT"})
|
@McpToolParam(description = "승인 처리 구분 (예: APPROVE, REJECT)")
|
||||||
|
@Schema(required = true, allowableValues = {"APPROVE", "REJECT"}, example = "APPROVE")
|
||||||
private String approvalStatus;
|
private String approvalStatus;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@@ -29,6 +33,8 @@ import lombok.Setter;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class BillingStatusRequest {
|
public class BillingStatusRequest {
|
||||||
|
|
||||||
@McpParameter(description = "조회할 청구 접수 번호", required = true)
|
@McpToolParam(description = "조회할 청구 접수 번호", required = true)
|
||||||
|
@Schema(example = "BILL20260805")
|
||||||
private String billingId;
|
private String billingId;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@@ -29,6 +33,8 @@ import lombok.Setter;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class BondCheckRequest {
|
public class BondCheckRequest {
|
||||||
|
|
||||||
@McpParameter(description = "확인하고자 하는 디지털 증권 발행 금액", required = true)
|
@McpToolParam(description = "확인하고자 하는 디지털 증권 발행 금액", required = true)
|
||||||
private Long amount;
|
@Schema(pattern = "^[0-9]+$", example = "1000000")
|
||||||
|
private String amount;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@@ -30,11 +33,12 @@ import lombok.Setter;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class BondIssueRequest {
|
public class BondIssueRequest {
|
||||||
|
|
||||||
@McpParameter(description = "발행할 디지털 증권 금액", required = true)
|
@McpToolParam(description = "발행할 증권 금액 (숫자 문자열)", required = true)
|
||||||
@McpValidation(minimum = 1)
|
@Schema(pattern = "^[0-9]+$", example = "1000000")
|
||||||
private Long amount;
|
private String amount;
|
||||||
|
|
||||||
@McpParameter(description = "발행 대상 계좌 번호", required = true)
|
|
||||||
@McpValidation(pattern = "\\S")
|
@McpToolParam(description = "발행 대상 계좌 번호", required = true)
|
||||||
|
@Schema(pattern = "\\S", example = "1234567890")
|
||||||
private String targetAccount;
|
private String targetAccount;
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,10 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.mcc.biz.cmm.dto
|
|
||||||
* @className ClaimSearchRequest
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpAnyOf;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -34,40 +20,39 @@ import lombok.Setter;
|
|||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@McpAnyOf({"claimNo", "contractNo"})
|
|
||||||
public class ClaimSearchRequest {
|
public class ClaimSearchRequest {
|
||||||
|
|
||||||
@McpParameter(description = "청구번호. CLM 다음 숫자 13자리 형식이다.")
|
@McpToolParam(description = "청구번호. CLM 다음 숫자 13자리 형식이다.")
|
||||||
@McpValidation(pattern = "^CLM[0-9]{13}$", examples = {"CLM2026070100123"})
|
@Schema(pattern = "^CLM[0-9]{13}$", example = "CLM2026070100123")
|
||||||
private String claimNo;
|
private String claimNo;
|
||||||
|
|
||||||
@McpParameter(description = "계약번호. 숫자 11자리 형식이다.")
|
@McpToolParam(description = "계약번호. 숫자 11자리 형식이다.")
|
||||||
@McpValidation(pattern = "^[0-9]{11}$", examples = {"10023456789"})
|
@Schema(pattern = "^[0-9]{11}$", example = "10023456789")
|
||||||
private String contractNo;
|
private String contractNo;
|
||||||
|
|
||||||
@McpParameter(description = "청구 상태 필터")
|
@McpToolParam(description = "청구 상태 필터")
|
||||||
@McpValidation(allowedValues = {
|
@Schema(example = "RECEIVED", allowableValues = {
|
||||||
"RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED",
|
"RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED",
|
||||||
"APPROVED", "PAID", "REJECTED", "WITHDRAWN"
|
"APPROVED", "PAID", "REJECTED", "WITHDRAWN"
|
||||||
})
|
})
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
@McpParameter(description = "청구 유형 필터")
|
@McpToolParam(description = "청구 유형 필터")
|
||||||
@McpValidation(allowedValues = {
|
@Schema(example = "MEDICAL", allowableValues = {
|
||||||
"MEDICAL", "SURGERY", "HOSPITALIZATION",
|
"MEDICAL", "SURGERY", "HOSPITALIZATION",
|
||||||
"DIAGNOSIS", "DEATH", "DISABILITY"
|
"DIAGNOSIS", "DEATH", "DISABILITY"
|
||||||
})
|
})
|
||||||
private String claimType;
|
private String claimType;
|
||||||
|
|
||||||
@McpParameter(description = "접수일 조회 시작일(YYYY-MM-DD)")
|
@McpToolParam(description = "접수일 조회 시작일(YYYY-MM-DD)")
|
||||||
@McpValidation(format = "date", examples = {"2026-01-01"})
|
@Schema(format = "date", example = "2026-01-01")
|
||||||
private String fromDate;
|
private String fromDate;
|
||||||
|
|
||||||
@McpParameter(description = "접수일 조회 종료일(YYYY-MM-DD)")
|
@McpToolParam(description = "접수일 조회 종료일(YYYY-MM-DD)")
|
||||||
@McpValidation(format = "date", examples = {"2026-07-31"})
|
@Schema(format = "date", example = "2026-07-31")
|
||||||
private String toDate;
|
private String toDate;
|
||||||
|
|
||||||
@McpParameter(description = "반환할 최대 건수")
|
@McpToolParam(description = "반환할 최대 건수 (기본값: 20, 최대: 50)")
|
||||||
@McpValidation(minimum = 1, maximum = 50, defaultValue = "20")
|
@Schema(pattern = "^[1-9][0-9]?$|^50$", defaultValue = "20", example = "20")
|
||||||
private Integer size;
|
private String size;
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,10 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.mcc.biz.cmm.dto
|
|
||||||
* @className ClaimSearchResponse
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpOutputSchema;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
@@ -36,46 +22,45 @@ import lombok.Setter;
|
|||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@McpOutputSchema
|
|
||||||
public class ClaimSearchResponse {
|
public class ClaimSearchResponse {
|
||||||
|
|
||||||
@McpParameter(description = "Execution result code.")
|
@McpToolParam(description = "Execution result code.")
|
||||||
@McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"})
|
@Schema(required = true, allowableValues = {"SUCCESS", "FAILURE"})
|
||||||
private String resultCode;
|
private String resultCode;
|
||||||
|
|
||||||
@McpParameter(description = "User-readable label for resultCode.")
|
@McpToolParam(description = "User-readable label for resultCode.")
|
||||||
@McpValidation(required = true, maxLength = 100)
|
@Schema(required = true, maxLength = 100)
|
||||||
private String resultLabel;
|
private String resultLabel;
|
||||||
|
|
||||||
@McpParameter(description = "Current claim processing status code.")
|
@McpToolParam(description = "Current claim processing status code.")
|
||||||
@McpValidation(required = true, allowedValues = {
|
@Schema(required = true, allowableValues = {
|
||||||
"RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED",
|
"RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED",
|
||||||
"APPROVED", "PAID", "REJECTED", "WITHDRAWN"
|
"APPROVED", "PAID", "REJECTED", "WITHDRAWN"
|
||||||
})
|
})
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
@McpParameter(description = "User-readable label for status.")
|
@McpToolParam(description = "User-readable label for status.")
|
||||||
@McpValidation(required = true, maxLength = 100)
|
@Schema(required = true, maxLength = 100)
|
||||||
private String statusLabel;
|
private String statusLabel;
|
||||||
|
|
||||||
@McpParameter(description = "Approved amount. Null before review; do not interpret null as zero.")
|
@McpToolParam(description = "Approved amount. Null before review; do not interpret null as zero.")
|
||||||
@McpValidation(minimum = 0, nullable = true)
|
@Schema(minimum = "0", nullable = true)
|
||||||
private Long approvedAmount;
|
private Long approvedAmount;
|
||||||
|
|
||||||
@McpParameter(description = "Present only when status is REJECTED; otherwise null.")
|
@McpToolParam(description = "Present only when status is REJECTED; otherwise null.")
|
||||||
@McpValidation(maxLength = 200, nullable = true)
|
@Schema(maxLength = 200, nullable = true)
|
||||||
private String rejectionReason;
|
private String rejectionReason;
|
||||||
|
|
||||||
@McpParameter(description = "Claim summaries, ordered by received date descending.")
|
@McpToolParam(description = "Claim summaries, ordered by received date descending.")
|
||||||
@McpValidation(required = true)
|
@Schema(required = true)
|
||||||
private List<ClaimSummary> items;
|
private List<ClaimSummary> items;
|
||||||
|
|
||||||
@McpParameter(description = "True when additional results exist beyond this response.")
|
@McpToolParam(description = "True when additional results exist beyond this response.")
|
||||||
@McpValidation(required = true)
|
@Schema(required = true)
|
||||||
private Boolean hasMore;
|
private Boolean hasMore;
|
||||||
|
|
||||||
@McpParameter(description = "Total number of matched claims.")
|
@McpToolParam(description = "Total number of matched claims.")
|
||||||
@McpValidation(required = true, minimum = 0)
|
@Schema(required = true, nullable = true)
|
||||||
private Integer totalCount;
|
private Integer totalCount;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@@ -85,20 +70,18 @@ public class ClaimSearchResponse {
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public static class ClaimSummary {
|
public static class ClaimSummary {
|
||||||
|
|
||||||
@McpParameter(description = "Claim processing status code.")
|
@McpToolParam(description = "Claim processing status code.")
|
||||||
@McpValidation(required = true)
|
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
@McpParameter(description = "User-readable label for status.")
|
@McpToolParam(description = "User-readable label for status.")
|
||||||
@McpValidation(required = true)
|
|
||||||
private String statusLabel;
|
private String statusLabel;
|
||||||
|
|
||||||
@McpParameter(description = "Received date in YYYY-MM-DD format.")
|
@McpToolParam(description = "Received date in YYYY-MM-DD format.")
|
||||||
@McpValidation(required = true, format = "date")
|
@Schema(required = true, format = "date")
|
||||||
private String receivedDate;
|
private String receivedDate;
|
||||||
|
|
||||||
@McpParameter(description = "Approved amount. Null before review; do not interpret null as zero.")
|
@McpToolParam(description = "Approved amount. Null before review; do not interpret null as zero.")
|
||||||
@McpValidation(minimum = 0, nullable = true)
|
@Schema(nullable = true)
|
||||||
private Long approvedAmount;
|
private Long approvedAmount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@@ -29,9 +33,12 @@ import lombok.Setter;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class ContractDetailRequest {
|
public class ContractDetailRequest {
|
||||||
|
|
||||||
@McpParameter(description = "고객명", required = true)
|
@McpToolParam(description = "고객명", required = true)
|
||||||
|
@Schema(example = "홍길동")
|
||||||
private String customerName;
|
private String customerName;
|
||||||
|
|
||||||
@McpParameter(description = "조회할 계약 번호", required = true)
|
|
||||||
|
@McpToolParam(description = "조회할 계약 번호", required = true)
|
||||||
|
@Schema(example = "1234567890")
|
||||||
private String contractId;
|
private String contractId;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@@ -29,9 +33,11 @@ import lombok.Setter;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class ContractStatusRequest {
|
public class ContractStatusRequest {
|
||||||
|
|
||||||
@McpParameter(description = "고객명", required = true)
|
@McpToolParam(description = "고객명", required = true)
|
||||||
|
@Schema(example = "홍길동")
|
||||||
private String customerName;
|
private String customerName;
|
||||||
|
|
||||||
@McpParameter(description = "조회할 계약 번호")
|
|
||||||
|
@McpToolParam(description = "조회할 계약 번호")
|
||||||
private String contractId;
|
private String contractId;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@@ -29,9 +33,11 @@ import lombok.Setter;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class CustomerDetailRequest {
|
public class CustomerDetailRequest {
|
||||||
|
|
||||||
@McpParameter(description = "고객명", required = true)
|
@McpToolParam(description = "고객명", required = true)
|
||||||
|
@Schema(example = "홍길동")
|
||||||
private String customerName;
|
private String customerName;
|
||||||
|
|
||||||
@McpParameter(description = "고객 식별 번호 (CID)")
|
|
||||||
|
@McpToolParam(description = "고객 식별 번호 (CID)")
|
||||||
private String customerId;
|
private String customerId;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@@ -29,9 +33,11 @@ import lombok.Setter;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class CustomerGradeRequest {
|
public class CustomerGradeRequest {
|
||||||
|
|
||||||
@McpParameter(description = "고객 이름 (예: 김신한)", required = true)
|
@McpToolParam(description = "고객 이름 (예: 김신한)", required = true)
|
||||||
|
@Schema(example = "홍길동")
|
||||||
private String customerName;
|
private String customerName;
|
||||||
|
|
||||||
@McpParameter(description = "고객 식별 번호 (CID)")
|
|
||||||
|
@McpToolParam(description = "고객 식별 번호 (CID)")
|
||||||
private String customerId;
|
private String customerId;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@@ -29,6 +33,8 @@ import lombok.Setter;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class LeaveCountRequest {
|
public class LeaveCountRequest {
|
||||||
|
|
||||||
@McpParameter(description = "연차 내역을 조회할 사원 번호", required = true)
|
@McpToolParam(description = "연차 내역을 조회할 사원 번호", required = true)
|
||||||
|
@Schema(example = "EMP12345")
|
||||||
private String employeeId;
|
private String employeeId;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.mcc.biz.cmm.dto
|
|
||||||
* @className MciSampleStringResponse
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import io.shinhanlife.glow.GlowMciFieldInfo;
|
import io.shinhanlife.glow.GlowMciFieldInfo;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.mcc.biz.cmm.dto
|
|
||||||
* @className MciSampleTargetDto
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import io.shinhanlife.glow.GlowMciFieldInfo;
|
import io.shinhanlife.glow.GlowMciFieldInfo;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,12 +25,15 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class MetaCommonCodeRequest {
|
public class MetaCommonCodeRequest {
|
||||||
@McpParameter(description = "통합코드 그룹 ID (예: GRP_SYS_01, GRP_COMM_CD)", required = false)
|
@McpToolParam(description = "통합코드 그룹 ID (예: GRP_SYS_01, GRP_COMM_CD)", required = false)
|
||||||
|
@Schema(example = "GRP_001")
|
||||||
private String groupCode;
|
private String groupCode;
|
||||||
|
|
||||||
@McpParameter(description = "코드명 검색 키워드 (예: 사용, 상태)", required = false)
|
|
||||||
|
@McpToolParam(description = "코드명 검색 키워드 (예: 사용, 상태)", required = false)
|
||||||
private String codeName;
|
private String codeName;
|
||||||
|
|
||||||
@McpParameter(description = "사용여부 (예: Y, N)", required = false)
|
@McpToolParam(description = "사용여부 (예: Y, N)", required = false)
|
||||||
|
@Schema(example = "Y")
|
||||||
private String useYn;
|
private String useYn;
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,12 +25,16 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class MetaTableRequest {
|
public class MetaTableRequest {
|
||||||
@McpParameter(description = "테이블 물리명 키워드 (예: TB_CUST_BAS, TB_CONT)", required = false)
|
@McpToolParam(description = "테이블 물리명 키워드 (예: TB_CUST_BAS, TB_CONT)", required = false)
|
||||||
|
@Schema(example = "TB_USER")
|
||||||
private String tableName;
|
private String tableName;
|
||||||
|
|
||||||
@McpParameter(description = "테이블 논리명(한글) 키워드 (예: 고객기본, 계약)", required = false)
|
|
||||||
|
@McpToolParam(description = "테이블 논리명(한글) 키워드 (예: 고객기본, 계약)", required = false)
|
||||||
|
@Schema(example = "고객기본")
|
||||||
private String tableLogicalName;
|
private String tableLogicalName;
|
||||||
|
|
||||||
@McpParameter(description = "스키마/소유자명 (예: DAPADM, SHLOWN)", required = false)
|
@McpToolParam(description = "스키마/소유자명 (예: DAPADM, SHLOWN)", required = false)
|
||||||
|
@Schema(example = "DAPADM")
|
||||||
private String owner;
|
private String owner;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -20,5 +22,6 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class SampleStringRequest {
|
public class SampleStringRequest {
|
||||||
|
@Schema(example = "test query")
|
||||||
private String query;
|
private String query;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
@@ -31,5 +33,6 @@ public class TemplateDownloadRequest {
|
|||||||
/**
|
/**
|
||||||
* 다운로드할 템플릿의 종류 ID (예: CUSTOMER_EXCEL, PRODUCT_PDF 등)
|
* 다운로드할 템플릿의 종류 ID (예: CUSTOMER_EXCEL, PRODUCT_PDF 등)
|
||||||
*/
|
*/
|
||||||
|
@Schema(example = "TPL_001")
|
||||||
private String templateId;
|
private String templateId;
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@@ -30,11 +33,12 @@ import lombok.Setter;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class VacationRegisterRequest {
|
public class VacationRegisterRequest {
|
||||||
|
|
||||||
@McpParameter(description = "연차를 등록할 사원 번호", required = true)
|
@McpToolParam(description = "연차를 등록할 사원 번호", required = true)
|
||||||
@McpValidation(pattern = "\\S")
|
@Schema(pattern = "\\S", example = "EMP12345")
|
||||||
private String employeeId;
|
private String employeeId;
|
||||||
|
|
||||||
@McpParameter(description = "휴가 일자 (YYYY-MM-DD 형식)", required = true)
|
|
||||||
@McpValidation(pattern = "^\\d{4}-\\d{2}-\\d{2}$")
|
@McpToolParam(description = "휴가 일자 (YYYY-MM-DD 형식)", required = true)
|
||||||
|
@Schema(pattern = "^\\d{4}-\\d{2}-\\d{2}$", example = "2026-08-05")
|
||||||
private String date;
|
private String date;
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,12 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
|
|
||||||
* @className BalanceUseCase
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
import org.springframework.ai.mcp.annotation.McpTool;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpTool;
|
import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
|
||||||
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
|
||||||
|
|
||||||
@McpTool(routingType = "MCI", categoryKey = "cmm")
|
|
||||||
public interface BalanceUseCase {
|
public interface BalanceUseCase {
|
||||||
@McpFunction(register = false, displayName = "balance 툴", name = "oth.cmm.balance.inquiry",
|
@McpTool(name = "oth.cmm.balance.inquiry", title = "잔고 조회 툴", description = "고객의 계좌 잔액을 조회합니다.")
|
||||||
description = "고객의 계좌 잔액을 조회합니다.",
|
@ToolHint(register = false, categoryKey = "cmm", mappingId = "ACC_001")
|
||||||
prompt = "고객 계좌 잔액을 조회해줘.",
|
|
||||||
mappingId = "ACC_001"
|
|
||||||
)
|
|
||||||
Object execute(BalanceRequest req);
|
Object execute(BalanceRequest req);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,13 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
||||||
|
|
||||||
/**
|
import org.springframework.ai.mcp.annotation.McpTool;
|
||||||
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
|
import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||||
* @className BillingProcessUseCase
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpTool;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
|
||||||
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
|
||||||
|
|
||||||
@McpTool(
|
|
||||||
routingType = "MCI",
|
|
||||||
categoryKey = "cmm"
|
|
||||||
)
|
|
||||||
public interface BillingProcessUseCase {
|
public interface BillingProcessUseCase {
|
||||||
Object getStatus(BillingStatusRequest req);
|
Object getStatus(BillingStatusRequest req);
|
||||||
|
|
||||||
@McpFunction(register = false, displayName = "process 툴", name = "oth.cmm.billing.process", description = "청구 처리", prompt = "현재 접수된 청구건에 대한 심사 처리를 진행해.", mappingId = "BILL_002")
|
@McpTool(name = "oth.cmm.billing.process", title = "청구 프로세스 툴", description = "청구 처리")
|
||||||
|
@ToolHint(register = false, categoryKey = "cmm", mappingId = "BILL_002")
|
||||||
Object processBilling(BillingProcessRequest data);
|
Object processBilling(BillingProcessRequest data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,14 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
|
|
||||||
* @className BondIssueUseCase
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
import org.springframework.ai.mcp.annotation.McpTool;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpTool;
|
import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
|
||||||
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
|
||||||
|
|
||||||
@McpTool(
|
|
||||||
routingType = "EAI",
|
|
||||||
categoryKey = "cmm"
|
|
||||||
)
|
|
||||||
public interface BondIssueUseCase {
|
public interface BondIssueUseCase {
|
||||||
Object check(BondCheckRequest req);
|
Object check(BondCheckRequest req);
|
||||||
|
|
||||||
@McpFunction(displayName = "issue 툴", name = "oth.cmm.bond.issue", register = false, description = "증권 발행 테스트1", prompt = "디지털 증권 발행 프로세스를 실행해.", mappingId = "BOND_002")
|
@McpTool(name = "oth.cmm.bond.issue", title = "채권 발행 툴", description = "증권 발행 테스트1")
|
||||||
|
@ToolHint(register = false, categoryKey = "cmm", mappingId = "BOND_002")
|
||||||
Object issue(BondIssueRequest data);
|
Object issue(BondIssueRequest data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,16 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
||||||
|
|
||||||
/**
|
import org.springframework.ai.mcp.annotation.McpTool;
|
||||||
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
|
import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||||
* @className ClaimSearchSchemaSampleUseCase
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpTool;
|
|
||||||
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest;
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest;
|
||||||
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
|
||||||
|
|
||||||
@McpTool(
|
|
||||||
routingType = "DIRECT",
|
|
||||||
categoryKey = "cmm"
|
|
||||||
)
|
|
||||||
public interface ClaimSearchSchemaSampleUseCase {
|
public interface ClaimSearchSchemaSampleUseCase {
|
||||||
|
|
||||||
@McpFunction(
|
@McpTool(name = "oth.cmm.claim.search", title = "청구 조회 스키마 샘플", description = "Claim search Tool sample using input and output JSON Schema resources.", annotations = @McpTool.McpAnnotations(readOnlyHint = true, idempotentHint = true))
|
||||||
register = false,
|
@ToolHint(register = false, categoryKey = "cmm",
|
||||||
displayName = "Claim search JSON Schema sample",
|
inputSchemaResource = "classpath:mcp/schema/claim-search-resource-input-schema.json",
|
||||||
name = "oth.cmm.claim.search",
|
outputSchemaResource = "classpath:mcp/schema/claim-search-resource-output-schema.json")
|
||||||
description = "Claim search Tool sample using input and output JSON Schema resources.",
|
|
||||||
prompt = "Search an insurance claim by claim number or contract number.",
|
|
||||||
inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json",
|
|
||||||
outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json",
|
|
||||||
readOnlyHint = true,
|
|
||||||
idempotentHint = true
|
|
||||||
)
|
|
||||||
ClaimSearchResponse search(ClaimSearchRequest request);
|
ClaimSearchResponse search(ClaimSearchRequest request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,18 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
|
|
||||||
* @className CommonUtilityUseCase
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
import org.springframework.ai.mcp.annotation.McpTool;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpTool;
|
import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
|
||||||
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
|
||||||
|
|
||||||
@McpTool(
|
|
||||||
routingType = "HTTP",
|
|
||||||
categoryKey = "cmm"
|
|
||||||
)
|
|
||||||
public interface CommonUtilityUseCase {
|
public interface CommonUtilityUseCase {
|
||||||
Object registerVacation(VacationRegisterRequest req);
|
Object registerVacation(VacationRegisterRequest req);
|
||||||
|
|
||||||
@McpFunction(register = false, displayName = "get_leave_count 툴", name = "oth.cmm.leave.count", description = "연차 갯수 조회", prompt = "현재 사용 가능한 남은 연차 일수를 알려줘.", mappingId = "HR_VAC_02")
|
@McpTool(name = "oth.cmm.leave.count", title = "공통 유틸리티 툴", description = "연차 갯수 조회")
|
||||||
|
@ToolHint(register = false, categoryKey = "cmm", mappingId = "HR_VAC_02")
|
||||||
Object getLeaveCount(LeaveCountRequest data);
|
Object getLeaveCount(LeaveCountRequest data);
|
||||||
|
|
||||||
@McpFunction(register = false, displayName = "secret_tool 툴", name = "oth.cmm.secret.execute", description = "비공개 툴 테스트", prompt = "숨겨진 툴 강제 호출", mappingId = "SECRET_001", visible = false)
|
@McpTool(name = "oth.cmm.secret.execute", title = "공통 유틸리티 툴", description = "비공개 툴 테스트")
|
||||||
|
@ToolHint(register = false, categoryKey = "cmm", mappingId = "SECRET_001")
|
||||||
Object secretTool(LeaveCountRequest data);
|
Object secretTool(LeaveCountRequest data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,14 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
|
|
||||||
* @className ContractInquiryUseCase
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
import org.springframework.ai.mcp.annotation.McpTool;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpTool;
|
import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
|
||||||
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
|
||||||
|
|
||||||
@McpTool(
|
|
||||||
routingType = "HTTP",
|
|
||||||
categoryKey = "cmm"
|
|
||||||
)
|
|
||||||
public interface ContractInquiryUseCase {
|
public interface ContractInquiryUseCase {
|
||||||
Object getStatus(ContractStatusRequest req);
|
Object getStatus(ContractStatusRequest req);
|
||||||
|
|
||||||
@McpFunction(register = false, displayName = "contract_detail 툴", name = "oth.cmm.contract.detail", description = "계약상세 조회", prompt = "김신한 고객의 계약 상세 내역을 알려줘.", mappingId = "CNTR_002")
|
@McpTool(name = "oth.cmm.contract.detail", title = "계약 상세조회 툴", description = "계약상세 조회")
|
||||||
|
@ToolHint(register = false, categoryKey = "cmm", mappingId = "CNTR_002")
|
||||||
Object getDetail(ContractDetailRequest data);
|
Object getDetail(ContractDetailRequest data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,14 @@
|
|||||||
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
|
|
||||||
* @className CustomerInfoUseCase
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
import org.springframework.ai.mcp.annotation.McpTool;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpTool;
|
import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
|
||||||
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
|
||||||
|
|
||||||
@McpTool(
|
|
||||||
routingType = "TCP",
|
|
||||||
categoryKey = "cmm"
|
|
||||||
)
|
|
||||||
public interface CustomerInfoUseCase {
|
public interface CustomerInfoUseCase {
|
||||||
Object getGrade(CustomerGradeRequest req);
|
Object getGrade(CustomerGradeRequest req);
|
||||||
|
|
||||||
@McpFunction(register = false, displayName = "detail 툴", name = "oth.cmm.customer.detail", description = "고객상세 정보 조회", prompt = "이 고객의 상세 기본정보(주소, 연락처 등)를 알려줘.", mappingId = "CRM_002")
|
@McpTool(name = "oth.cmm.customer.detail", title = "고객 상세조회 툴", description = "고객상세 정보 조회")
|
||||||
|
@ToolHint(register = false, categoryKey = "cmm", mappingId = "CRM_002")
|
||||||
Object getDetail(CustomerDetailRequest data);
|
Object getDetail(CustomerDetailRequest data);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user