31 Commits

Author SHA1 Message Date
mjh
05ea5f9aaa merge: feature/20260805_mjh into feature/20260805_emis_create 2026-08-05 09:41:09 +09:00
mjh
50315ad245 feat: add ONCSC1340 MCI client and IO DTOs 2026-08-05 09:33:58 +09:00
jade
65222b5b1a feat: index.html 메인 화면에 Tester, Console 카드 추가
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m49s
2026-08-05 00:06:02 +09:00
jade
b9f98e89ce fix: index.html 헤더 링크 추가 및 tester.html CSV 한글 깨짐 방지(BOM 추가)
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m49s
2026-08-05 00:01:59 +09:00
jade
96e308aefb fix: ZtUsacRepository (MyBatis Mapper) 의존성 주입 에러 수정 (MybatisConfig에 @MapperScan 추가)
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m52s
2026-08-04 23:56:23 +09:00
jade
96a26bc93d fix: ToolManifestService 등의 Bean 로드 에러 방지를 위해 scanBasePackages 범위를 io.shinhanlife.dap.lib 전체로 확장
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m50s
2026-08-04 23:53:58 +09:00
jade
a2586fd6d3 fix: ToolPodMcpToolSynchronizer가 dap-gateway에서도 스캔되어 발생하는 Bean 에러 수정 (usecase 패키지로 이동)
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m48s
2026-08-04 23:51:07 +09:00
jade
cdfb6ef901 fix: ToolRegistryHeartbeatSender Bean을 인식하지 못하는 런타임 에러 수정 (scanBasePackages 추가)
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m53s
2026-08-04 23:46:30 +09:00
jade
3729e2d6ee fix: McpRouterController, ChatController 불필요한 FQCN 선언 제거 및 import 통합
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m52s
2026-08-04 23:43:44 +09:00
jade
946ec6d70f chore: DapToolOthApplication, logback 등 코드 내 남아있던 이전 명칭(찌꺼기) 일괄 치환 및 정리
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m7s
2026-08-04 23:36:50 +09:00
jade
136ad2fa71 refactor: dap-tool-* 모듈을 dap-was-* 이름으로 전면 개편 및 컨테이너명/파이프라인 연동
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m57s
2026-08-04 23:33:06 +09:00
jade
3e69436bfe chore: 잘못 생성되어 있던 중복 ToolMetadata 클래스(mcg.dto 패키지) 삭제
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m54s
2026-08-04 23:10:02 +09:00
jade
f827760513 refactor: 공용 클래스(MCP, Manifest, UseCase 등) lib 패키지로 이관 및 통합
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m17s
2026-08-04 23:06:01 +09:00
jade
1a70495686 feat(ui): add tool-test-console and tester dashboards
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m51s
- Add Auto-Tester Dashboard (tester.html) in gateway for batch testing tools

- Add standalone Tool Test Console (tool-test-console.html) in core

- Fix Tailwind CSS Preflight conflicts in console UI

- Update console payload schema resolution to support both gateway and pod modes

- Strip JSON-RPC metadata from tool payload output in console

- Add unified navigation headers across all static HTML pages
2026-08-04 18:24:52 +09:00
jade
39c1f4ee12 UI: chat.html 상단 GNB 디자인을 scaffold.html과 동일하게 동기화
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m52s
2026-08-04 16:45:57 +09:00
jade
e858859021 feat: standardize tool names by pod domain action
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m55s
2026-08-04 16:39:03 +09:00
jade
a58af1606e feat: publish tool manifest endpoint
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m50s
2026-08-04 15:58:59 +09:00
jade
08ccc043da feat: organize tool schemas by category
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m58s
2026-08-04 14:42:08 +09:00
jade
4393071212 chore: log validated tool response
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m54s
2026-08-04 13:38:43 +09:00
jade
1bac8d9d87 feat: enrich claim search output schema
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m53s
2026-08-04 13:34:02 +09:00
jade
31e6fd605d feat: support MCP output schemas
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m1s
2026-08-04 11:24:59 +09:00
jade
15262d5a0f test: keep input schemas in tool modules
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m56s
2026-08-04 11:03:35 +09:00
jade
2957272cc3 feat: support explicit MCP input schemas
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m18s
2026-08-04 10:50:25 +09:00
jade
52f585fa45 Refactor: Remove SensitiveDataMasker and fix PaginationRequestValidator
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m55s
2026-08-03 17:41:02 +09:00
jade
bc86f36825 feat(scaffold): 템플릿 코드에 @McpValidation 예시 추가
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m42s
2026-08-03 16:37:11 +09:00
jade
55dcffa2c9 feat(schema): 고급 JSON Schema 제약조건(format, default, examples 등) 지원 및 McpFunction 수정 반영
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-03 16:35:46 +09:00
jade
be0314afe8 feat(schema): @McpAnyOf 어노테이션 및 JSON Schema 파싱 로직 추가
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m56s
2026-08-03 16:31:43 +09:00
jade
39eab0429e Revert "feat(security): ToolSecurityContext 및 KMS 복호화 공통 모듈 추가"
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m1s
This reverts commit cf958622bd.
2026-08-03 15:35:20 +09:00
jade
cf958622bd feat(security): ToolSecurityContext 및 KMS 복호화 공통 모듈 추가
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m18s
2026-08-03 13:45:16 +09:00
jade
d858f2ab90 Merge branch 'main' of https://git.devjun.net/kimhyungsik/ax_hub_mcp_tool
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m53s
2026-07-31 14:59:17 +09:00
jade
2bc896a130 application 업데이트 2026-07-31 14:59:00 +09:00
300 changed files with 3379 additions and 471 deletions

View File

@@ -59,7 +59,7 @@ jobs:
# 4. 마운트된 /app 디렉토리로 이동하여 호스트의 도커 컴포즈 제어!
cd /app
docker system prune -f
ACTIVE_PROFILE=dev docker compose up -d --build --remove-orphans gateway redis mci-mock dozzle tool-sms tool-oth
ACTIVE_PROFILE=dev docker compose up -d --build --remove-orphans gateway redis mci-mock dozzle was-sms was-oth
# 5. 배포 후 대롱대롱 매달려 있는 가비지 이미지 자동 소거 청소!
docker image prune -f

139
README.md
View File

@@ -466,4 +466,141 @@ MCP SDK 표준 tools/list, tools/call
---
문서에 없는 업무·보안·배포 기준은 임의로 추가하지 말고 AA 및 플랫폼 운영 기준과 먼저 합의합니다.
문서에 없는 업무·보안·배포 기준은 임의로 추가하지 말고 AA 및 플랫폼 운영 기준과 먼저 합의합니다.
## Input/Output Schema 작성 가이드
Tool Schema는 Agent가 Tool을 정확히 호출하고, 반환값의 의미를 일관되게 해석하도록 하는 계약입니다. 인증 정보·사번·주민번호 등 민감정보(PII)는 Input/Output Schema와 Tool 응답에 포함하지 않습니다.
### Input Schema
Input Schema는 Agent가 Tool에 전달하는 파라미터의 이름, 타입, 필수 여부, 허용값, 형식 등을 정의합니다.
적용 우선순위는 다음과 같습니다.
1. `inputSchemaResource` — 복잡한 규칙을 담은 JSON Schema 리소스
2. `inputSchema` — 어노테이션에 직접 선언한 JSON Schema
3. 요청 DTO 필드의 `@McpValidation` — 자동 JSON Schema 생성
단순한 요청 DTO는 `@McpValidation`만으로 관리합니다.
```java
public class ClaimSearchRequest {
@McpValidation(required = true, pattern = "^CLM[0-9]{13}$")
private String claimNo;
@McpValidation(minimum = 1, maximum = 100)
private Integer size;
}
```
### Output Schema
Output Schema는 Tool이 반환하는 결과의 타입과 의미를 정의합니다. `BusinessToolController`는 Tool 실행 후 반환값을 Output Schema 기준으로 검증합니다.
적용 우선순위는 다음과 같습니다.
1. `outputSchemaResource` — 조건부 필드·중첩 배열 등 복잡한 규칙을 담은 JSON Schema 리소스
2. `outputSchema` — 어노테이션에 직접 선언한 JSON Schema
3. 반환 DTO의 `@McpOutputSchema`와 필드 `@McpValidation` — 자동 JSON Schema 생성
4. 위 설정이 모두 없으면 Output Schema 검증을 수행하지 않음
따라서 단순한 응답은 별도 `outputSchemaResource` 없이 반환 DTO에 `@McpOutputSchema`를 선언하면 됩니다. `null`이 정상 값일 수 있는 필드는 `nullable = true`를 반드시 지정합니다.
```java
@McpOutputSchema
public class ClaimSearchResponse {
@McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"})
private String resultCode;
@McpValidation(nullable = true, minimum = 0)
private Long approvedAmount;
}
```
### 복잡한 Schema는 Tool 모듈별 리소스로 관리
조건부 응답, 중첩 DTO, 배열 정렬 기준처럼 어노테이션만으로 표현하기 어려운 규칙은 Tool Core가 아니라 각 Tool 모듈의 리소스에 JSON Schema로 둡니다.
```text
src/main/resources/
└─ tool-schemas/
└─ {categoryKey}/
├─ claim-search-resource-input-schema.json
└─ claim-search-resource-output-schema.json
```
예를 들어 `categoryKey``cmm`이면 아래와 같이 선언합니다.
```java
@McpFunction(
name = "oth.cmm.claim.search",
inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json",
outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json"
)
public ClaimSearchResponse search(ClaimSearchRequest request) {
// ...
}
```
`inputSchemaResource``outputSchemaResource`는 복잡한 경우에만 선언합니다. 단순한 Tool까지 JSON 파일을 별도 생성할 필요는 없습니다.
### Output 설계 규칙
- 코드와 표시용 라벨을 함께 반환합니다. 예: `status` + `statusLabel`
- `null`이 정상인 값은 의미를 설명에 명시하고 DTO에는 `nullable = true`를 설정합니다.
- 조건부 필드는 어떤 조건에서 값이 존재하는지 JSON Schema에 명시합니다.
- 배열은 정렬 기준을 설명에 명시합니다. 예: `접수일 내림차순`
- 목록 응답에는 추가 조회 여부를 나타내는 `hasMore`를 포함합니다.
- 민감정보는 마스킹보다 **응답에서 제외**하는 것을 우선합니다.
### 실행 로그 및 확인
Tool 실행이 끝나면 아래 로그는 Schema 정의가 아니라 **검증을 통과한 실제 최종 응답값**을 출력합니다.
```text
[Tool -> MCP Gateway] Output Schema Result: { ... }
```
따라서 로그에도 실제 응답이 남으므로, 응답 DTO와 Output Schema에 민감정보가 포함되지 않도록 설계해야 합니다.
스키마 리소스와 DTO 기반 자동 Schema는 아래 테스트로 함께 검증할 수 있습니다.
```powershell
.\gradlew.bat :dap-tool-oth:test --tests "io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequestSchemaTest"
```
### Tool Naming Convention
All Tool names use the four-level lowercase format `pod.domain.service.action`. Do not use underscores or CamelCase; use a hyphen (`-`) only when a single level has multiple words.
- `pod`: deployment Tool Pod/module (`dap-tool-oth``oth`, `dap-tool-sms``sms`)
- `domain`: business-domain package (`cmm`, `smp`, `sol`, etc.)
- `service`: business service or resource
- `action`: the requested operation (`search`, `list`, `detail`, `issue`, `inquiry`, etc.)
```text
oth.cmm.bond.issue
oth.cmm.claim.search
oth.sol.request.list
oth.smp.weather.inquiry
```
When Scaffold receives `dap-tool-oth`, `cmm`, and `ClaimSearch`, it generates `oth.cmm.claim.search`. The `validateMcpToolNames` Gradle task rejects both a duplicate name and any name outside this format before packaging, including its source file and line number.
### Tool Test Console
각 Tool Pod는 공통 테스트 화면을 제공합니다.
```text
http://localhost:8084/tool-test-console.html
```
화면은 현재 Pod의 `/tool-manifest`에서 Tool 목록과 `inputSchema`를 읽습니다. Tool을 선택한 뒤 `Schema 샘플 채우기`로 요청 JSON을 만들고 실행할 수 있습니다. 업무에 맞게 보정한 요청은 `현재 요청 저장`으로 브라우저의 `localStorage`에 보관합니다.
`Run saved cases`는 저장된 테스트 케이스를 순차 실행해 성공/실패, HTTP 상태, 소요 시간을 보여줍니다. 따라서 Tool이 수백 개여도 각 Tool마다 테스트 화면을 만들 필요 없이, 유효한 업무 테스트 데이터만 한 번 저장하면 이후에는 몇 번의 클릭으로 회귀 테스트할 수 있습니다.
- Tool 호출은 현재 Pod의 `/mcp/{toolName}`로 수행합니다.
- 매 실행마다 `trace-id`, `request-id`를 새로 생성하여 응답과 함께 표시합니다.
- 외부 MCI/EAI Tool은 샘플값 대신 개발계에서 허용된 테스트 데이터를 저장해서 사용해야 합니다.

View File

@@ -50,7 +50,7 @@ subprojects {
}
}
def toolCoreProject = project(':dap-tool-core')
def toolCoreProject = project(':dap-was-lib')
tasks.register('validateMcpToolNames', JavaExec) {
group = 'verification'

View File

@@ -3,7 +3,7 @@ plugins {
}
dependencies {
implementation project(':dap-tool-core')
implementation project(':dap-was-lib')
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

View File

@@ -16,7 +16,6 @@ package io.shinhanlife.dap.mcg.audit;
* </pre>
*/
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
import io.shinhanlife.dap.mcg.security.McpRequestContext;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
@@ -32,11 +31,8 @@ import org.springframework.stereotype.Service;
public class AuditLogService {
private static final Logger audit = LoggerFactory.getLogger("MCP_AUDIT");
private final McpGatewayProperties properties;
private final SensitiveDataMasker masker;
public AuditLogService(McpGatewayProperties properties, SensitiveDataMasker masker) {
public AuditLogService(McpGatewayProperties properties) {
this.properties = properties;
this.masker = masker;
}
/**
@@ -47,7 +43,7 @@ public class AuditLogService {
return;
}
audit.info("event=tool_started requestId={} agentId={} userId={} clientAddress={} tool={} arguments={}",
context.requestId(), context.agentId(), context.userId(), context.clientAddress(), toolName, masker.mask(arguments));
context.requestId(), context.agentId(), context.userId(), context.clientAddress(), toolName, arguments);
}
/**

View File

@@ -1,6 +1,6 @@
package io.shinhanlife.dap.mcg.config;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;

View File

@@ -17,7 +17,7 @@ package io.shinhanlife.dap.mcg.guardrail;
*/
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.stereotype.Service;

View File

@@ -1,92 +0,0 @@
package io.shinhanlife.dap.mcg.guardrail;
/**
* @package io.shinhanlife.dap.mcg.guardrail
* @className SensitiveDataMasker
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Audit Log, Redis Trace, Agent 응답 preview에 남으면 안 되는 민감정보를 마스킹합니다.
*/
@Component
public class SensitiveDataMasker {
private static final Set<String> SENSITIVE_KEYS = Set.of(
"password", "passwd", "pwd", "token", "accessToken", "refreshToken", "secret",
"ssn", "rrn", "residentNumber", "cardNumber", "accountNumber", "accountNo",
"phone", "mobile", "email", "idempotencyKey");
private static final Pattern EMAIL = Pattern.compile("([a-zA-Z0-9._%+-]{2})[a-zA-Z0-9._%+-]*(@[a-zA-Z0-9.-]+)");
private static final Pattern CARD_OR_ACCOUNT = Pattern.compile("\\b(\\d{4})\\d{4,12}(\\d{2,4})\\b");
private final ObjectMapper json;
public SensitiveDataMasker(ObjectMapper json) {
this.json = json;
}
/**
* JsonNode 전체를 재귀적으로 순회하며 민감 key와 민감 패턴을 마스킹합니다.
*/
public JsonNode mask(JsonNode input) {
if (input == null || input.isMissingNode() || input.isNull()) {
return json.createObjectNode();
}
if (input.isArray()) {
ArrayNode masked = json.createArrayNode();
for (JsonNode item : input) {
masked.add(mask(item));
}
return masked;
}
if (input.isObject()) {
ObjectNode masked = json.createObjectNode();
Iterator<Map.Entry<String, JsonNode>> fields = input.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> entry = fields.next();
String key = entry.getKey();
JsonNode value = entry.getValue();
if (isSensitiveKey(key)) {
masked.put(key, "***");
} else {
masked.set(key, mask(value));
}
}
return masked;
}
if (input.isTextual()) {
return json.valueToTree(maskText(input.asText()));
}
return input;
}
private boolean isSensitiveKey(String key) {
return key != null && SENSITIVE_KEYS.stream().anyMatch(sensitive -> sensitive.equalsIgnoreCase(key));
}
private String maskText(String value) {
if (value == null || value.isBlank()) {
return value;
}
String masked = EMAIL.matcher(value).replaceAll("$1***$2");
return CARD_OR_ACCOUNT.matcher(masked).replaceAll("$1********$2");
}
}

View File

@@ -17,7 +17,7 @@ package io.shinhanlife.dap.mcg.guardrail;
*/
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

View File

@@ -15,7 +15,8 @@ package io.shinhanlife.dap.mcg.presentation;
*
* </pre>
*/
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import lombok.RequiredArgsConstructor;
@@ -56,7 +57,7 @@ public class ChatController {
try {
List<ToolCallback> callbacks = new ArrayList<>();
io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse toolsResponse = mcpRouterController.listTools(null).getBody();
JsonRpcResponse toolsResponse = mcpRouterController.listTools(null).getBody();
if (toolsResponse != null && toolsResponse.getResult() instanceof Map) {
Map<String, Object> resultMap = (Map<String, Object>) toolsResponse.getResult();
if (resultMap.containsKey("tools")) {

View File

@@ -20,7 +20,7 @@ import io.shinhanlife.dap.lib.adapter.dto.JsonRpcRequest;
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dap.lib.adapter.dto.Params;
import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;
@@ -91,7 +91,7 @@ public class McpRouterController {
try {
Object result = executeService.execute(payload, effectiveTenantId);
io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse response = new io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse();
JsonRpcResponse response = new JsonRpcResponse();
response.setJsonrpc("2.0");
response.setId(payload.containsKey("id") ? String.valueOf(payload.get("id")) : UUID.randomUUID().toString());
response.setResult(result);

View File

@@ -34,7 +34,7 @@ public class ScaffoldingController {
@PostMapping("/pod")
public String scaffoldPod(@RequestBody Map<String, String> req) {
try {
String moduleName = req.getOrDefault("moduleName", "dap-tool-oth");
String moduleName = req.getOrDefault("moduleName", "dap-was-oth");
if (!moduleName.startsWith("dap-tool-")) moduleName = "dap-tool-" + moduleName;
String port = req.getOrDefault("port", "8085");
String shortName = moduleName.replace("dap-tool-", "").replace("-", "");
@@ -57,7 +57,7 @@ public class ScaffoldingController {
String description = req.get("description");
String group = req.getOrDefault("categoryKey", req.getOrDefault("group", "COMMON"));
String routingType = req.getOrDefault("routingType", "HTTP");
String moduleName = req.getOrDefault("moduleName", "dap-tool-oth");
String moduleName = req.getOrDefault("moduleName", "dap-was-oth");
String author = req.get("author");
if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name");
String date = req.get("date");
@@ -94,15 +94,15 @@ public class ScaffoldingController {
if (sourceDir == null) sourceDir = System.getProperty("user.dir");
File dir = new File(sourceDir);
File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("dap-tool-") && !f.getName().equals("dap-tool-core"));
File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("dap-tool-") && !f.getName().equals("dap-was-lib"));
if (files == null || files.length == 0) {
return List.of("dap-tool-oth", "dap-tool-hr", "dap-tool-sms");
return List.of("dap-was-oth", "dap-tool-hr", "dap-was-sms");
}
return Arrays.stream(files).map(File::getName).sorted().collect(Collectors.toList());
} catch (Exception e) {
return List.of("dap-tool-oth", "dap-tool-hr", "dap-tool-sms");
return List.of("dap-was-oth", "dap-tool-hr", "dap-was-sms");
}
}
}

View File

@@ -16,9 +16,8 @@ package io.shinhanlife.dap.mcg.redis;
* </pre>
*/
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
import io.shinhanlife.dap.mcg.security.McpRequestContext;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
@@ -49,19 +48,16 @@ public class RedisToolTraceService {
private final ObjectProvider<StringRedisTemplate> redisProvider;
private final ObjectMapper json;
private final McpMonitorEventService monitorEvents;
private final SensitiveDataMasker masker;
private final Map<String, AttemptState> attemptStates = new ConcurrentHashMap<>();
public RedisToolTraceService(McpGatewayProperties properties,
ObjectProvider<StringRedisTemplate> redisProvider,
ObjectMapper json,
McpMonitorEventService monitorEvents,
SensitiveDataMasker masker) {
McpMonitorEventService monitorEvents) {
this.properties = properties;
this.redisProvider = redisProvider;
this.json = json;
this.monitorEvents = monitorEvents;
this.masker = masker;
}
/**
@@ -206,7 +202,7 @@ public class RedisToolTraceService {
arguments.fieldNames().forEachRemaining(argNames::add);
trace.put("argumentNames", argNames);
trace.put("arguments", masker.mask(arguments).toString());
trace.put("arguments", arguments.toString());
trace.put("responseSummary", responseSummary(responseText));
trace.put("timestamp", Instant.now().toString());
return json.writeValueAsString(trace);

View File

@@ -1,6 +1,6 @@
package io.shinhanlife.dap.mcg.registry;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;

View File

@@ -18,8 +18,8 @@ package io.shinhanlife.dap.mcg.security;
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.mcg.dto.OperationType;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.lib.dto.OperationType;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import org.springframework.stereotype.Service;
import java.util.Set;

View File

@@ -17,13 +17,12 @@ package io.shinhanlife.dap.mcg.service;
*/
import java.util.HashMap;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.RetryPolicy;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.mcg.dto.OperationType;
import io.shinhanlife.dap.lib.dto.OperationType;
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
import io.shinhanlife.dap.mcg.guardrail.GuardrailService;
import io.shinhanlife.dap.mcg.security.McpRequestContext;
import io.shinhanlife.dap.mcg.security.McpRequestContextResolver;
@@ -58,7 +57,6 @@ public class ExecuteService {
private final ToolPlanner planner;
private final KillSwitchService killSwitchService;
private final ObjectMapper objectMapper;
private final SensitiveDataMasker dataMasker;
private final GuardrailService guardrailService;
private final McpRequestContextResolver contextResolver;
private final AuditLogService auditLogService;
@@ -76,7 +74,6 @@ public class ExecuteService {
public ExecuteService(ToolPlanner planner,
KillSwitchService killSwitchService,
ObjectMapper objectMapper,
SensitiveDataMasker dataMasker,
GuardrailService guardrailService,
McpRequestContextResolver contextResolver,
AuditLogService auditLogService,
@@ -92,7 +89,6 @@ public class ExecuteService {
this.planner = planner;
this.killSwitchService = killSwitchService;
this.objectMapper = objectMapper;
this.dataMasker = dataMasker;
this.guardrailService = guardrailService;
this.contextResolver = contextResolver;
this.auditLogService = auditLogService;
@@ -266,14 +262,14 @@ public class ExecuteService {
headers.put("trace-id", context.requestId());
headers.put("request-id", java.util.UUID.randomUUID().toString());
ObjectNode pageArguments = paginationValidator.normalize(arguments);
ObjectNode pageArguments = paginationValidator.normalize(metadata, arguments);
LargeToolResponseService.Collector collector = largeResponses.newCollector(metadata.getName(), context.requestId());
while (true) {
Map<String, Object> pagePayload = objectMapper.convertValue(pageArguments, Map.class);
try {
log.info(" [ExecuteService] 요청 페이로드(마스킹 적용): {}", objectMapper.writeValueAsString(dataMasker.mask(objectMapper.valueToTree(pagePayload))));
log.info(" [ExecuteService] 요청 페이로드: {}", objectMapper.writeValueAsString(pagePayload));
} catch (Exception ignore) {}
JsonNode data = null;

View File

@@ -2,7 +2,7 @@ package io.shinhanlife.dap.mcg.service;
import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@@ -18,7 +18,7 @@ package io.shinhanlife.dap.mcg.sync;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpSyncServer;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.sync.CustomWebMvcSseServerTransportProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

View File

@@ -15,7 +15,7 @@ package io.shinhanlife.dap.mcg.sync;
*
* </pre>
*/
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;

View File

@@ -1,6 +1,6 @@
package io.shinhanlife.dap.mcg.sync;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.modelcontextprotocol.server.McpSyncServer;
import org.slf4j.Logger;

View File

@@ -19,7 +19,6 @@ import java.util.Iterator;
import java.util.Map;
import io.shinhanlife.dap.mcg.config.AgentResponseBudgetProperties;
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -30,12 +29,9 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
public class AgentResponseBudgetService {
private final AgentResponseBudgetProperties properties;
private final ObjectMapper json;
private final SensitiveDataMasker masker;
public AgentResponseBudgetService(AgentResponseBudgetProperties properties, ObjectMapper json, SensitiveDataMasker masker) {
public AgentResponseBudgetService(AgentResponseBudgetProperties properties, ObjectMapper json) {
this.properties = properties;
this.json = json;
this.masker = masker;
}
public ObjectNode apply(ObjectNode response) {
@@ -109,7 +105,7 @@ public class AgentResponseBudgetService {
}
private JsonNode budgetItem(JsonNode item, BudgetStats stats) {
JsonNode masked = masker.mask(item);
JsonNode masked = item;
if (!masked.isObject()) {
return truncateByBytes(masked, stats);
}

View File

@@ -18,7 +18,6 @@ package io.shinhanlife.dap.mcg.tool.large;
import java.util.Iterator;
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import org.springframework.stereotype.Service;
@@ -34,16 +33,13 @@ import java.time.Instant;
public class LargeToolResponseService {
private final McpGatewayProperties properties;
private final ObjectMapper json;
private final SensitiveDataMasker masker;
private final AgentResponseBudgetService agentBudget;
public LargeToolResponseService(McpGatewayProperties properties,
ObjectMapper json,
SensitiveDataMasker masker,
AgentResponseBudgetService agentBudget) {
this.properties = properties;
this.json = json;
this.masker = masker;
this.agentBudget = agentBudget;
}
@@ -157,8 +153,7 @@ public class LargeToolResponseService {
}
Page page = pageFrom(data);
if (!page.paginated() && pageCount == 0 && count(page.items()) <= pageSize()) {
JsonNode masked = masker.mask(data);
normalData = masked;
normalData = data;
pageCount = 1;
returnedCount = count(page.items());
totalCount = returnedCount;
@@ -271,19 +266,18 @@ public class LargeToolResponseService {
}
private JsonNode previewItem(JsonNode item) {
JsonNode masked = masker.mask(item);
long itemBytes = jsonBytes(masked);
long itemBytes = jsonBytes(item);
if (itemBytes <= properties.largeResponseMaxItemBytes()) {
return masked;
return item;
}
truncated = true;
ObjectNode preview = json.createObjectNode();
preview.put("truncated", true);
preview.put("originalBytes", itemBytes);
preview.put("maxItemBytes", properties.largeResponseMaxItemBytes());
if (masked.isObject()) {
if (item.isObject()) {
ArrayNode fieldNames = json.createArrayNode();
Iterator<String> fieldNamesIter = masked.fieldNames();
Iterator<String> fieldNamesIter = item.fieldNames();
while (fieldNamesIter.hasNext()) {
fieldNames.add(fieldNamesIter.next());
}

View File

@@ -15,6 +15,7 @@ package io.shinhanlife.dap.mcg.tool.large;
*
* </pre>
*/
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
@@ -40,12 +41,18 @@ public class PaginationRequestValidator {
/**
* pageSize/cursor를 검증한 뒤 Tool 서버에 넘길 안전한 arguments 복사본을 만듭니다.
*/
public ObjectNode normalize(ObjectNode arguments) {
public ObjectNode normalize(ToolMetadata metadata, ObjectNode arguments) {
try {
ObjectNode normalized = arguments == null
? json.createObjectNode()
: (ObjectNode) json.readTree(json.writeValueAsString(arguments));
normalizePageSize(normalized);
if (metadata != null && metadata.allowedArguments().contains("pageSize")) {
normalizePageSize(normalized);
} else if (normalized.has("pageSize")) {
normalizePageSize(normalized);
}
validateCursor(normalized);
return normalized;
} catch (ToolExecutionException error) {

View File

@@ -15,7 +15,6 @@ package io.shinhanlife.dap.mcg.tool.result;
*
* </pre>
*/
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -34,11 +33,8 @@ public class ToolExecutionResultFormatter {
private static final int TEXT_PREVIEW_LIMIT = 2_000;
private final ObjectMapper json;
private final SensitiveDataMasker masker;
public ToolExecutionResultFormatter(ObjectMapper json, SensitiveDataMasker masker) {
public ToolExecutionResultFormatter(ObjectMapper json) {
this.json = json;
this.masker = masker;
}
/**
@@ -71,7 +67,7 @@ public class ToolExecutionResultFormatter {
}
public ToolExecutionResult fromJson(String toolName, JsonNode parsed, long sizeBytes) {
JsonNode masked = masker.mask(parsed);
JsonNode masked = parsed;
if (masked.isObject()) {
ObjectNode object = (ObjectNode) masked;
if (object.path("isError").asBoolean(false) || object.has("error") || object.has("failureType")) {

View File

@@ -10,10 +10,10 @@
mcp:
gateway:
fallback:
default-url: http://tool-oth:8084
default-url: http://was-oth:8084
routes:
sms: http://tool-sms:8082
hr: http://tool-oth:8084
sms: http://was-sms:8082
hr: http://was-oth:8084
# --- 신한라이프 EAI/MCI 연계 IP 정보 (개발 환경) ---
shinhan:

View File

@@ -26,10 +26,10 @@ server:
mcp:
gateway:
fallback:
default-url: http://tool-oth:8084
default-url: http://was-oth:8084
routes:
sms: http://tool-sms:8082
hr: http://tool-oth:8084
sms: http://was-sms:8082
hr: http://was-oth:8084
agent-claims-required: false
trusted-claims-required: false
write-approval-required: false

View File

@@ -381,6 +381,8 @@
<a href="/catalog.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Catalog</a>
<a href="/playground.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Playground</a>
<a href="/chat.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Chat</a>
<a href="/tester.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Tester</a>
<a href="/tool-test-console.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Console</a>
</nav>
</div>
<div class="flex items-center">

View File

@@ -70,6 +70,8 @@
<a href="/catalog.html" style="color:#ffffff;" class="font-semibold">Catalog</a>
<a href="/playground.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Playground</a>
<a href="/chat.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Chat</a>
<a href="/tester.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Tester</a>
<a href="/tool-test-console.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Console</a>
</nav>
</div>
<div class="flex items-center">

View File

@@ -42,10 +42,35 @@
.markdown-content th, .markdown-content td { padding: 0.4rem; border: 1px solid #475569; text-align: left; }
</style>
</head>
<body class="h-screen flex flex-col items-center justify-center p-4">
<body class="h-screen flex flex-col bg-[#0f1115]">
<!-- Top Navigation -->
<header style="border-bottom: 1px solid #27272a; background: rgba(9,9,11,0.85); backdrop-filter: blur(16px);" class="sticky top-0 z-50">
<div class="max-w-6xl mx-auto px-6 h-14 flex items-center justify-between">
<div class="flex items-center space-x-5">
<a href="/index.html" class="flex items-center group">
<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 Gateway</span>
</a>
<div class="h-4 w-px" style="background:#27272a;"></div>
<nav class="flex space-x-5 text-[13px] font-medium">
<a href="/admin/scaffold.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Scaffold</a>
<a href="/catalog.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Catalog</a>
<a href="/playground.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Playground</a>
<a href="/chat.html" style="color:#ffffff;" class="font-semibold">Chat</a>
<a href="/tester.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Tester</a>
<a href="/tool-test-console.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Console</a>
</nav>
</div>
<div class="flex items-center">
<span class="text-[10px] uppercase tracking-widest px-2 py-1 rounded font-bold" style="background:rgba(59,130,246,0.1); color:#60a5fa; border:1px solid rgba(59,130,246,0.2);">v0.0.1</span>
</div>
</div>
</header>
<!-- Chat Container -->
<div class="w-full max-w-3xl h-[85vh] flex flex-col bg-[#16181d] rounded-2xl shadow-2xl overflow-hidden border border-white/5 relative">
<!-- Main Content Area -->
<main class="flex-1 flex flex-col items-center justify-center p-4 w-full">
<!-- Chat Container -->
<div class="w-full max-w-3xl w-full flex-1 max-h-[85vh] flex flex-col bg-[#16181d] rounded-2xl shadow-2xl overflow-hidden border border-white/5 relative">
<!-- Header -->
<div class="px-6 py-4 border-b border-white/5 flex items-center justify-between bg-[#16181d] z-10">
@@ -111,6 +136,7 @@
</div>
</div>
</main>
<!-- 로딩 인디케이터 템플릿 -->
<template id="loading-template">

View File

@@ -101,6 +101,8 @@
<a href="/catalog.html" class="text-zinc-400 hover:text-white transition-colors">Catalog</a>
<a href="/playground.html" class="text-zinc-400 hover:text-white transition-colors">Playground</a>
<a href="/chat.html" class="text-zinc-400 hover:text-white transition-colors">Chat</a>
<a href="/tester.html" class="text-zinc-400 hover:text-white transition-colors">Tester</a>
<a href="/tool-test-console.html" class="text-zinc-400 hover:text-white transition-colors">Console</a>
</nav>
</div>
<div class="flex items-center">
@@ -122,7 +124,7 @@
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 w-full max-w-6xl">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full max-w-6xl">
<!-- Scaffold Card -->
<a href="/admin/scaffold.html" class="card-panel p-8 group block">
<div class="w-12 h-12 icon-box rounded-xl flex items-center justify-center mb-6 text-zinc-300">
@@ -178,6 +180,34 @@
Start Chatting <svg class="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
</div>
</a>
<!-- Tester Card -->
<a href="/tester.html" class="card-panel p-8 group block">
<div class="w-12 h-12 icon-box rounded-xl flex items-center justify-center mb-6 text-zinc-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path></svg>
</div>
<h2 class="text-lg font-semibold text-zinc-100 mb-3">Auto Tester</h2>
<p class="text-[13px] text-zinc-400 leading-relaxed">
Batch execute all registered tools with auto-generated dummy data to verify stability. Export results to CSV for reporting.
</p>
<div class="mt-6 flex items-center text-[12px] font-semibold text-zinc-300 group-hover:text-blue-400 group-hover:translate-x-1 transition-all">
Run Tests <svg class="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
</div>
</a>
<!-- Console Card -->
<a href="/tool-test-console.html" class="card-panel p-8 group block">
<div class="w-12 h-12 icon-box rounded-xl flex items-center justify-center mb-6 text-zinc-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
</div>
<h2 class="text-lg font-semibold text-zinc-100 mb-3">Test Console</h2>
<p class="text-[13px] text-zinc-400 leading-relaxed">
Advanced developer console for executing tools with custom JSON payloads. Monitor real-time logs and debug application state.
</p>
<div class="mt-6 flex items-center text-[12px] font-semibold text-zinc-300 group-hover:text-blue-400 group-hover:translate-x-1 transition-all">
Open Console <svg class="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
</div>
</a>
</div>
</main>
</body>

View File

@@ -112,6 +112,8 @@
<a href="/catalog.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Catalog</a>
<a href="/playground.html" style="color:#ffffff;" class="font-semibold">Playground</a>
<a href="/chat.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Chat</a>
<a href="/tester.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Tester</a>
<a href="/tool-test-console.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Console</a>
</nav>
</div>
<div class="flex items-center space-x-3">

View File

@@ -0,0 +1,347 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tool Auto Tester</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
* { font-family: 'Geist', sans-serif; }
body { background-color: #09090b; color: #f4f4f5; }
.geist-mono { font-family: 'Geist Mono', monospace; }
.btn-execute {
background: linear-gradient(135deg, #3b82f6, #6366f1);
color: #ffffff;
font-weight: 600;
padding: 10px 24px;
border-radius: 8px;
font-size: 14px;
border: none;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 0 20px rgba(59, 130, 246, 0.3);
}
.btn-execute:hover { box-shadow: 0 0 30px rgba(99, 102, 241, 0.5); transform: translateY(-1px); }
.btn-execute:disabled { background: #27272a; color: #52525b; box-shadow: none; transform: none; cursor: not-allowed; }
.btn-sm {
background: #27272a;
color: #d4d4d8;
font-weight: 500;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
border: 1px solid #3f3f46;
cursor: pointer;
transition: background 0.1s;
}
.btn-sm:hover { background: #3f3f46; color: white; }
.btn-sm:disabled { opacity: 0.5; cursor: not-allowed; }
.card { background: #18181b; border: 1px solid #3f3f46; border-radius: 12px; }
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
.table th { text-align: left; padding: 12px 16px; border-bottom: 1px solid #3f3f46; color: #a1a1aa; font-weight: 500; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
.table td { padding: 12px 16px; border-bottom: 1px solid #27272a; vertical-align: middle; }
.table tr:last-child td { border-bottom: none; }
.table tr:hover td { background-color: rgba(255,255,255,0.02); }
.status-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.02em;
}
.status-idle { background: rgba(113, 113, 122, 0.15); color: #a1a1aa; border: 1px solid rgba(113, 113, 122, 0.3); }
.status-running { background: rgba(59, 130, 246, 0.15); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.3); }
.status-success { background: rgba(34, 197, 94, 0.15); color: #4ade80; border: 1px solid rgba(34, 197, 94, 0.3); }
.status-error { background: rgba(239, 68, 68, 0.15); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.3); }
.cat-badge {
font-family: 'Geist Mono', monospace;
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
background: #27272a;
color: #a1a1aa;
border: 1px solid #3f3f46;
}
</style>
</head>
<body class="min-h-screen" style="overflow-y: scroll;">
<header style="border-bottom: 1px solid #27272a; background: rgba(9,9,11,0.85); backdrop-filter: blur(16px);" class="sticky top-0 z-50">
<div class="max-w-6xl mx-auto px-6 h-14 flex items-center justify-between">
<div class="flex items-center space-x-5">
<a href="/index.html" class="flex items-center group">
<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 Gateway</span>
</a>
<div class="h-4 w-px" style="background:#27272a;"></div>
<nav class="flex space-x-5 text-[13px] font-medium">
<a href="/admin/scaffold.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Scaffold</a>
<a href="/catalog.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Catalog</a>
<a href="/playground.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Playground</a>
<a href="/chat.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Chat</a>
<a href="/tester.html" style="color:#ffffff;" class="font-semibold">Tester</a>
<a href="/tool-test-console.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Console</a>
</nav>
</div>
<div class="flex items-center">
<span class="text-[10px] uppercase tracking-widest px-2 py-1 rounded font-bold" style="background:rgba(59,130,246,0.1); color:#60a5fa; border:1px solid rgba(59,130,246,0.2);">v0.0.1</span>
</div>
</div>
</header>
<main class="max-w-5xl mx-auto px-6 py-10">
<div class="flex items-center justify-between mb-8">
<div>
<h1 class="text-2xl font-bold tracking-tight mb-2" style="color:#f4f4f5;">Auto-Tester Dashboard</h1>
<p class="text-sm" style="color:#a1a1aa;">Batch execute all registered tools with auto-generated dummy data to verify stability.</p>
</div>
<div class="flex items-center gap-3">
<button id="exportCsvBtn" class="btn-sm flex items-center gap-2 h-10 px-4 text-sm" onclick="exportCsv()" style="display: none;">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
Export CSV
</button>
<button id="runAllBtn" class="btn-execute flex items-center gap-2" onclick="runAllTests()">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
Run All Tests
</button>
</div>
</div>
<div class="card overflow-hidden">
<div class="bg-[#09090b] px-6 py-4 border-b border-[#27272a] flex items-center justify-between">
<div class="text-sm font-semibold text-slate-300">Tool List</div>
<div class="text-xs text-slate-500 geist-mono" id="progressInfo">Total: 0 / Completed: 0</div>
</div>
<div class="overflow-x-auto">
<table class="table" id="toolsTable">
<thead>
<tr>
<th style="width:10%">Category</th>
<th style="width:25%">Tool Name</th>
<th style="width:25%">Auto-Generated Payload</th>
<th style="width:20%">Status</th>
<th style="width:20%" class="text-right">Action</th>
</tr>
</thead>
<tbody id="toolsTbody">
<tr>
<td colspan="5" class="text-center text-zinc-500 py-8">Loading tools...</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
<script>
let allTools = [];
let isRunningAll = false;
async function fetchTools() {
try {
const response = await fetch('/mcp/api/v1/tools/list');
const data = await response.json();
allTools = data.result?.tools || [];
renderTable();
updateProgress(0);
} catch (error) {
document.getElementById('toolsTbody').innerHTML = `<tr><td colspan="5" class="text-center text-red-400 py-8">Failed to load tools. Gateway might be down.</td></tr>`;
}
}
function generateDummyPayload(schema) {
if (!schema || !schema.properties) return {};
const payload = {};
for (const [key, value] of Object.entries(schema.properties)) {
if (value.example !== undefined) {
payload[key] = value.example;
} else if (value.examples && Array.isArray(value.examples) && value.examples.length > 0) {
payload[key] = value.examples[0];
} else if (value.default !== undefined) {
payload[key] = value.default;
} else {
if (value.type === 'string') {
if (value.enum && value.enum.length > 0) payload[key] = value.enum[0];
else payload[key] = "test_string";
} else if (value.type === 'integer' || value.type === 'number') {
payload[key] = value.minimum !== undefined ? value.minimum : 1;
} else if (value.type === 'boolean') {
payload[key] = true;
} else {
payload[key] = null;
}
}
}
return payload;
}
function renderTable() {
const tbody = document.getElementById('toolsTbody');
if (allTools.length === 0) {
tbody.innerHTML = `<tr><td colspan="5" class="text-center text-zinc-500 py-8">No tools found.</td></tr>`;
return;
}
allTools.sort((a, b) => (a.categoryKey || '').localeCompare(b.categoryKey || ''));
let html = '';
allTools.forEach((tool, index) => {
const payload = generateDummyPayload(tool.parametersSchema);
const payloadStr = JSON.stringify(payload);
const shortPayload = payloadStr.length > 30 ? payloadStr.substring(0, 30) + '...' : payloadStr;
html += `
<tr id="row-${index}">
<td><span class="cat-badge">${tool.categoryKey || 'oth'}</span></td>
<td class="font-medium text-slate-200">${tool.name}</td>
<td class="text-xs text-zinc-400 geist-mono" title='${payloadStr}'>${shortPayload}</td>
<td id="status-${index}"><span class="status-badge status-idle geist-mono">IDLE</span></td>
<td class="text-right">
<button class="btn-sm run-single-btn" onclick="runSingleTest(${index})">Test Single</button>
</td>
</tr>
`;
});
tbody.innerHTML = html;
}
function updateProgress(completed) {
document.getElementById('progressInfo').textContent = `Total: ${allTools.length} / Completed: ${completed}`;
}
async function executeCall(toolName, payload, index) {
const statusCell = document.getElementById(`status-${index}`);
statusCell.innerHTML = `<span class="status-badge status-running geist-mono">RUNNING...</span>`;
const reqPayload = { jsonrpc: "2.0", method: "tools/call", params: { name: toolName, arguments: payload }, id: Date.now() };
const startTime = Date.now();
try {
const response = await fetch('/mcp/api/v1/tools/call', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Agent-Id': 'AUTO-TESTER' },
body: JSON.stringify(reqPayload)
});
const latency = Date.now() - startTime;
allTools[index]._latency = latency;
if (response.ok) {
// API layer might return 200 OK but contain an error message in MCP format.
const data = await response.json();
let respData = data;
if (data?.result?.result) {
respData = data.result.result.data !== undefined ? data.result.result.data : data.result.result;
} else if (data?.error) {
respData = data.error;
}
allTools[index]._responseData = respData;
if (data.result && data.result.isError) {
statusCell.innerHTML = `<span class="status-badge status-error geist-mono">ERR: MCP ERROR &middot; ${latency}ms</span>`;
allTools[index]._lastStatus = "ERR: MCP ERROR";
} else {
statusCell.innerHTML = `<span class="status-badge status-success geist-mono">200 OK &middot; ${latency}ms</span>`;
allTools[index]._lastStatus = "200 OK";
}
} else {
statusCell.innerHTML = `<span class="status-badge status-error geist-mono">HTTP ${response.status} &middot; ${latency}ms</span>`;
allTools[index]._lastStatus = `HTTP ${response.status}`;
}
} catch (error) {
statusCell.innerHTML = `<span class="status-badge status-error geist-mono">NET ERR</span>`;
allTools[index]._lastStatus = "NET ERR";
allTools[index]._latency = 0;
}
}
async function runSingleTest(index) {
const tool = allTools[index];
const payload = generateDummyPayload(tool.parametersSchema);
const btn = document.querySelector(`#row-${index} .run-single-btn`);
btn.disabled = true;
await executeCall(tool.name, payload, index);
btn.disabled = false;
}
async function runAllTests() {
if (isRunningAll) return;
isRunningAll = true;
const btn = document.getElementById('runAllBtn');
btn.disabled = true;
btn.innerHTML = `<div class="spinner border-[2px]" style="width:16px;height:16px;border-top-color:white;margin-right:8px;"></div> Running...`;
// Reset statuses
for (let i = 0; i < allTools.length; i++) {
document.getElementById(`status-${i}`).innerHTML = `<span class="status-badge status-idle geist-mono">IDLE</span>`;
}
let completed = 0;
// Execute in batches to avoid overwhelming the gateway/services
const batchSize = 3;
for (let i = 0; i < allTools.length; i += batchSize) {
const batch = allTools.slice(i, i + batchSize);
const promises = batch.map((tool, idx) => {
const actualIndex = i + idx;
const payload = generateDummyPayload(tool.parametersSchema);
return executeCall(tool.name, payload, actualIndex).then(() => {
completed++;
updateProgress(completed);
});
});
await Promise.all(promises);
}
isRunningAll = false;
btn.disabled = false;
btn.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> Run All Tests`;
// Show Export CSV button after all tests run
document.getElementById('exportCsvBtn').style.display = 'flex';
}
function exportCsv() {
if (allTools.length === 0) return;
let csvContent = "Category,Tool Name,Status,Latency(ms),Payload,Response\n";
allTools.forEach(tool => {
const category = `"${(tool.categoryKey || 'oth').replace(/"/g, '""')}"`;
const name = `"${(tool.name || '').replace(/"/g, '""')}"`;
const status = `"${(tool._lastStatus || 'NOT RUN').replace(/"/g, '""')}"`;
const latency = tool._latency || 0;
const payload = generateDummyPayload(tool.parametersSchema);
const payloadStr = `"${JSON.stringify(payload).replace(/"/g, '""')}"`;
const resp = tool._responseData ? JSON.stringify(tool._responseData) : "";
const responseStr = `"${resp.replace(/"/g, '""')}"`;
csvContent += `${category},${name},${status},${latency},${payloadStr},${responseStr}\n`;
});
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", `mcp_tools_test_report_${new Date().toISOString().slice(0, 10)}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
document.addEventListener('DOMContentLoaded', fetchTools);
</script>
</body>
</html>

View File

@@ -5,7 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpSchema;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.sync.RegistryMcpToolSpecificationFactory;
import org.junit.jupiter.api.Test;

View File

@@ -1,74 +0,0 @@
package io.shinhanlife.dap.mcc.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.util.Map;
/**
* @package io.shinhanlife.dap.mcc.dto
* @className ToolMetadata
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ToolMetadata {
// 1. Tool 기본 정보
private String uid; // UUID 형식의 고유 식별자
private String semver; // 버전 (예: 1.0.0)
private String displayName; // 사람이 읽는 라벨 (1-128자)
private String name; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool)
private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능)
// 2. 파라미터 스키마 (JSON Schema 형태의 Map)
private Map<String, Object> parametersSchema;
// 2-0. 프론트엔드 UI용 함수별 프롬프트 매핑 (추가됨)
private Map<String, String> actionPrompts;
// 2-1. 도메인 부서 그룹명 (category_key, 슬러그 형식)
private String categoryKey;
private String endpoint;
private String podUrl;
private String integrationType;
private String mciServiceId;
@Builder.Default
private Boolean visible = true;
@Builder.Default
private Boolean isRegistered = true;
@Builder.Default
private Boolean requiresApproval = false;
@Builder.Default
private Boolean readOnlyHint = false;
@Builder.Default
private Boolean destructiveHint = false;
@Builder.Default
private Boolean idempotentHint = false;
@Builder.Default
private Boolean openWorldHint = false;
}

View File

@@ -1,87 +0,0 @@
package io.shinhanlife.dap.mcg.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* Tool(Agent)의 명세 및 라우팅 정보를 담고 있는 메타데이터 클래스
* Redis 레지스트리에 저장되며, Planner와 Router 간의 통신 객체(Plan)로 사용됩니다.
*/
/**
* @package io.shinhanlife.dap.mcg.dto
* @className ToolMetadata
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class ToolMetadata {
// 1. Tool 기본 정보
private String uid; // UUID 형식의 고유 식별자
private String semver; // 버전 (예: 1.0.0)
private String displayName; // 사람이 읽는 라벨 (1-128자)
private String name; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool)
private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능)
// 2. 파라미터 스키마 (JSON Schema 형태의 Map)
private Map<String, Object> parametersSchema;
// 2-0. 프론트엔드 UI용 함수별 프롬프트 매핑 (추가됨)
private Map<String, String> actionPrompts;
// 2-1. 도메인 부서 그룹명 (category_key, 슬러그 형식)
private String categoryKey;
// 2-2. 툴 처리 엔드포인트 URI 경로 (예: /api/tool/customer-info)
private String endpoint;
// 2-3. Pod 실행 URL (독립적인 Microservice 라우팅용, 예: http://localhost:8082)
private String podUrl;
// 2-4. 가시성 여부
@Builder.Default
private Boolean visible = true;
// 2-5. Redis 등록 여부 (UI 표출용)
@Builder.Default
private Boolean isRegistered = true;
// 2-6. HITL 승인 필요 여부
@Builder.Default
private Boolean requiresApproval = 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초당 허용 최대 요청 수
}

View File

@@ -0,0 +1,36 @@
package io.shinhanlife.dap.mcc.infra.itrf.mci.ncs.c;
import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent;
import io.shinhanlife.glow.communication.dto.Transfer;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* @package io.shinhanlife.dap.mcc.infra.itrf.mci.ncs.c
* @className MciNcsCClient
* @description 개인고객정보상세조회 MCI 호출 클라이언트
* @author KDK
* @create 2026.08.03
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.08.03 KDK 최초생성
*
* </pre>
*/
@Component
@RequiredArgsConstructor
public class MciNcsCClient {
private final AxhubMciComponent mci;
public <T> Transfer<T> callTo(
String interfaceId,
String dummy,
Object mciReq,
Class<T> resType
) throws Exception {
return mci.callTo(interfaceId, dummy, mciReq, resType);
}
}

View File

@@ -0,0 +1,43 @@
package io.shinhanlife.dap.mcc.infra.itrf.mci.ncs.c.io;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.shinhanlife.glow.communication.annotation.GlowTrgmField;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* @package io.shinhanlife.dap.mcc.infra.itrf.mci.ncs.c
* @className ONCSC1340_I
* @description 개인고객정보상세조회 InDto
* @author
* @create 2026.08.04
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
*
* </pre>
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ONCSC1340_I {
@GlowTrgmField(order = 1, length = 153, description = "개인고객정보상세조회InDto")
private IndvCsinDtptInqrInDto indvCsinDtptInqrInDto;
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public static class IndvCsinDtptInqrInDto {
@GlowTrgmField(order = 1, length = 3, description = "조회구분코드")
private String inqrScCd;
@GlowTrgmField(order = 2, length = 150, description = "고객조회내용")
private String cstInqrCt;
}
}

View File

@@ -0,0 +1,19 @@
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;
/**
* MCP 스키마 생성 시 anyOf (해당 필드들 중 최소 1개 이상 필수) 제약을 부여합니다.
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface McpAnyOf {
/**
* anyOf 제약에 포함될 필드명 목록
* 예: @McpAnyOf({"claimNo", "contractNo"})
*/
String[] value();
}

View File

@@ -26,11 +26,30 @@ public @interface McpFunction {
String description();
String prompt() default "";
String mappingId() default "";
// 추가: 해당 함수가 요구하는 비즈니스 파라미터(JSON 형태의 properties) 정의
/**
* 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 라우팅은 되나 목록에서 숨김)
@@ -44,5 +63,14 @@ public @interface McpFunction {
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)
}

View File

@@ -0,0 +1,17 @@
package io.shinhanlife.dap.lib.annotation;
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 {
}

View File

@@ -26,5 +26,11 @@ 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 {};
}

View File

@@ -26,5 +26,12 @@ import java.util.Map;
public class McpProperties {
private String namespace;
private Manifest manifest = new Manifest();
@Data
public static class Manifest {
private String bundleId;
private String namePrefix;
}
}

View File

@@ -21,7 +21,11 @@ import org.springframework.context.annotation.Configuration;
*
* </pre>
*/
import org.mybatis.spring.annotation.MapperScan;
import io.shinhanlife.glow.GlowMybatisMapper;
@Configuration
@MapperScan(basePackages = "io.shinhanlife.dap", annotationClass = GlowMybatisMapper.class)
public class MybatisConfig {
@Bean

View File

@@ -0,0 +1,18 @@
package io.shinhanlife.dap.lib.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Common MCP Tool Schema Bean configuration.
*/
@Configuration
public class ToolSchemaConfiguration {
@Bean
public ToolSchemaResolver toolSchemaResolver(ObjectMapper objectMapper) {
return new ToolSchemaResolver(objectMapper);
}
}

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.mcg.dto;
package io.shinhanlife.dap.lib.dto;
/**
* @package io.shinhanlife.dap.mcg.dto

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.mcg.dto;
package io.shinhanlife.dap.lib.dto;
import lombok.Getter;
import lombok.Setter;
@@ -62,6 +62,10 @@ public class ToolMetadata {
// 2-4. 가시성 여부
@Builder.Default
private Boolean visible = true;
// 활성화 여부
@Builder.Default
private Boolean enabled = true;
// 2-5. Redis 등록 여부 (UI 표출용)
@Builder.Default

Some files were not shown because too many files have changed in this diff Show More