Compare commits
31 Commits
b22cd779cf
...
feature/20
| Author | SHA1 | Date | |
|---|---|---|---|
| 05ea5f9aaa | |||
| 50315ad245 | |||
|
|
65222b5b1a | ||
|
|
b9f98e89ce | ||
|
|
96e308aefb | ||
|
|
96a26bc93d | ||
|
|
a2586fd6d3 | ||
|
|
cdfb6ef901 | ||
|
|
3729e2d6ee | ||
|
|
946ec6d70f | ||
|
|
136ad2fa71 | ||
|
|
3e69436bfe | ||
|
|
f827760513 | ||
|
|
1a70495686 | ||
|
|
39c1f4ee12 | ||
|
|
e858859021 | ||
|
|
a58af1606e | ||
|
|
08ccc043da | ||
|
|
4393071212 | ||
|
|
1bac8d9d87 | ||
|
|
31e6fd605d | ||
|
|
15262d5a0f | ||
|
|
2957272cc3 | ||
|
|
52f585fa45 | ||
|
|
bc86f36825 | ||
|
|
55dcffa2c9 | ||
|
|
be0314afe8 | ||
|
|
39eab0429e | ||
|
|
cf958622bd | ||
|
|
d858f2ab90 | ||
|
|
2bc896a130 |
@@ -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
139
README.md
@@ -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은 샘플값 대신 개발계에서 허용된 테스트 데이터를 저장해서 사용해야 합니다.
|
||||
|
||||
@@ -50,7 +50,7 @@ subprojects {
|
||||
}
|
||||
}
|
||||
|
||||
def toolCoreProject = project(':dap-tool-core')
|
||||
def toolCoreProject = project(':dap-was-lib')
|
||||
|
||||
tasks.register('validateMcpToolNames', JavaExec) {
|
||||
group = 'verification'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
347
dap-gateway/src/main/resources/static/tester.html
Normal file
347
dap-gateway/src/main/resources/static/tester.html
Normal 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 · ${latency}ms</span>`;
|
||||
allTools[index]._lastStatus = "ERR: MCP ERROR";
|
||||
} else {
|
||||
statusCell.innerHTML = `<span class="status-badge status-success geist-mono">200 OK · ${latency}ms</span>`;
|
||||
allTools[index]._lastStatus = "200 OK";
|
||||
}
|
||||
} else {
|
||||
statusCell.innerHTML = `<span class="status-badge status-error geist-mono">HTTP ${response.status} · ${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>
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
@@ -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초당 허용 최대 요청 수
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.shinhanlife.dap.mcg.dto;
|
||||
package io.shinhanlife.dap.lib.dto;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.dto
|
||||
@@ -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
Reference in New Issue
Block a user