Compare commits
40 Commits
b5ff930556
...
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 | ||
| b22cd779cf | |||
|
|
b3908f771f | ||
|
|
58234b1c59 | ||
|
|
541d6d1938 | ||
| 628925dce9 | |||
|
|
9d16b8ef3a | ||
|
|
39f68ccf46 | ||
|
|
1bf09705d1 | ||
|
|
60055dd9ed |
@@ -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은 샘플값 대신 개발계에서 허용된 테스트 데이터를 저장해서 사용해야 합니다.
|
||||
|
||||
17
build.gradle
17
build.gradle
@@ -49,3 +49,20 @@ subprojects {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
|
||||
def toolCoreProject = project(':dap-was-lib')
|
||||
|
||||
tasks.register('validateMcpToolNames', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Checks duplicate @McpFunction names across all Tool modules before packaging.'
|
||||
dependsOn toolCoreProject.tasks.named('classes')
|
||||
classpath = toolCoreProject.sourceSets.main.runtimeClasspath
|
||||
mainClass.set('io.shinhanlife.dap.lib.validation.McpToolNameValidationRunner')
|
||||
args rootProject.projectDir.absolutePath
|
||||
}
|
||||
|
||||
subprojects {
|
||||
tasks.matching { it.name == 'bootJar' }.configureEach {
|
||||
dependsOn rootProject.tasks.named('validateMcpToolNames')
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AX HUB - ChatClient</title>
|
||||
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>
|
||||
<script src="/vendor/marked.umd.js"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; background-color: #0f1115; color: #e2e8f0; }
|
||||
@@ -25,12 +26,51 @@
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-3px); }
|
||||
}
|
||||
|
||||
.markdown-content > :first-child { margin-top: 0; }
|
||||
.markdown-content > :last-child { margin-bottom: 0; }
|
||||
.markdown-content p { margin: 0.5rem 0; }
|
||||
.markdown-content ul, .markdown-content ol { margin: 0.5rem 0; padding-left: 1.5rem; }
|
||||
.markdown-content ul { list-style: disc; }
|
||||
.markdown-content ol { list-style: decimal; }
|
||||
.markdown-content a { color: #6ee7b7; text-decoration: underline; }
|
||||
.markdown-content code { padding: 0.125rem 0.3rem; border-radius: 0.25rem; background: #0f1115; color: #f1f5f9; }
|
||||
.markdown-content pre { margin: 0.75rem 0; padding: 0.75rem; overflow-x: auto; border-radius: 0.5rem; background: #0f1115; }
|
||||
.markdown-content pre code { padding: 0; background: transparent; }
|
||||
.markdown-content blockquote { margin: 0.5rem 0; padding-left: 0.75rem; border-left: 3px solid #34d399; color: #cbd5e1; }
|
||||
.markdown-content table { width: 100%; margin: 0.5rem 0; border-collapse: collapse; }
|
||||
.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">
|
||||
@@ -96,6 +136,7 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 로딩 인디케이터 템플릿 -->
|
||||
<template id="loading-template">
|
||||
@@ -150,7 +191,7 @@
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 max-w-[80%]">
|
||||
<span class="text-xs text-slate-500 ml-1 font-medium">Assistant</span>
|
||||
<div class="bg-[#1e2128] px-5 py-3.5 rounded-2xl rounded-tl-sm text-sm text-slate-200 leading-relaxed border border-white/5 shadow-sm whitespace-pre-wrap bot-text"></div>
|
||||
<div class="bg-[#1e2128] px-5 py-3.5 rounded-2xl rounded-tl-sm text-sm text-slate-200 leading-relaxed border border-white/5 shadow-sm markdown-content bot-text"></div>
|
||||
</div>
|
||||
`;
|
||||
chatBox.appendChild(div);
|
||||
@@ -184,6 +225,61 @@
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function sanitizeRenderedHtml(html) {
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = html;
|
||||
template.content.querySelectorAll('script, iframe, object, embed, style, link, meta').forEach(element => element.remove());
|
||||
template.content.querySelectorAll('*').forEach(element => {
|
||||
[...element.attributes].forEach(attribute => {
|
||||
const name = attribute.name.toLowerCase();
|
||||
const value = attribute.value.trim();
|
||||
if (name.startsWith('on') || name === 'style' ||
|
||||
(['href', 'src', 'xlink:href'].includes(name) && /^(javascript|data|vbscript):/i.test(value))) {
|
||||
element.removeAttribute(attribute.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
return template.innerHTML;
|
||||
}
|
||||
|
||||
function normalizeMarkdown(markdown) {
|
||||
// 일부 모델은 문단 뒤의 제목 표기(예: "설명입니다.## 제목") 앞 줄바꿈을 생략한다.
|
||||
// 또한 "###제목", "###📝 제목"처럼 해시 뒤 공백을 생략하는 출력도 제목으로 보정한다.
|
||||
return markdown
|
||||
.replace(/\r\n?/g, '\n')
|
||||
.replace(/([^\n])((?:#{1,6})\s+)/g, '$1\n$2')
|
||||
.replace(/(^|\n)(#{1,6})([^\s#])/g, '$1$2 $3');
|
||||
}
|
||||
|
||||
function renderMarkdown(container, markdown) {
|
||||
// 프로젝트에 포함한 표준 GFM 렌더러가 제목, 강조, 표, 목록, 코드, 링크 등을 일괄 처리한다.
|
||||
const normalizedMarkdown = normalizeMarkdown(markdown);
|
||||
if (typeof marked !== 'undefined') {
|
||||
const html = marked.parse(normalizedMarkdown, { breaks: true, gfm: true });
|
||||
container.innerHTML = sanitizeRenderedHtml(html);
|
||||
return;
|
||||
}
|
||||
|
||||
// 로컬 라이브러리 로딩에 실패한 경우에는 안전한 일반 텍스트로 표시한다.
|
||||
container.textContent = normalizedMarkdown;
|
||||
}
|
||||
|
||||
function consumeSseEvents(buffer, onData) {
|
||||
// Spring SSE는 응답 본문의 줄바꿈을 여러 data: 줄로 전송한다.
|
||||
// data 줄을 단순 연결하면 Markdown 표의 행 구분이 사라지므로, 이벤트 단위로 복원한다.
|
||||
let eventEnd;
|
||||
while ((eventEnd = buffer.indexOf('\n\n')) !== -1) {
|
||||
const event = buffer.substring(0, eventEnd);
|
||||
buffer = buffer.substring(eventEnd + 2);
|
||||
const dataLines = event.split('\n')
|
||||
.filter(line => line.startsWith('data:'))
|
||||
.map(line => line.substring(5));
|
||||
|
||||
if (dataLines.length > 0) onData(dataLines.join('\n'));
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const text = chatInput.value.trim();
|
||||
if (!text || isLoading) return;
|
||||
@@ -214,24 +310,24 @@
|
||||
// 4. 로딩 숨기고 봇 응답 컨테이너 생성
|
||||
hideLoading();
|
||||
const textContainer = appendBotMessageContainer();
|
||||
let responseText = '';
|
||||
|
||||
// 5. 스트림 읽기
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let sseBuffer = '';
|
||||
|
||||
const appendResponse = (data) => {
|
||||
responseText += data;
|
||||
renderMarkdown(textContainer, responseText);
|
||||
scrollToBottom();
|
||||
};
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
sseBuffer += decoder.decode(value || new Uint8Array(), { stream: !done }).replace(/\r\n/g, '\n');
|
||||
sseBuffer = consumeSseEvents(sseBuffer, appendResponse);
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
const lines = chunk.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data:')) {
|
||||
const data = line.substring(5);
|
||||
textContainer.innerHTML += escapeHtml(data);
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -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>
|
||||
44
dap-gateway/src/main/resources/static/vendor/MARKED-LICENSE.md
vendored
Normal file
44
dap-gateway/src/main/resources/static/vendor/MARKED-LICENSE.md
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# License information
|
||||
|
||||
## Contribution License Agreement
|
||||
|
||||
If you contribute code to this project, you are implicitly allowing your code
|
||||
to be distributed under the MIT license. You are also implicitly verifying that
|
||||
all code is your original work. `</legalese>`
|
||||
|
||||
## Marked
|
||||
|
||||
Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/)
|
||||
Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
## Markdown
|
||||
|
||||
Copyright © 2004, John Gruber
|
||||
http://daringfireball.net/
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
* Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.
|
||||
74
dap-gateway/src/main/resources/static/vendor/marked.umd.js
vendored
Normal file
74
dap-gateway/src/main/resources/static/vendor/marked.umd.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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