feat: Agent Builder 커스텀 JSON-RPC 스펙 및 에러코드 적용, McpBridge 문자열 ID 지원

This commit is contained in:
jade
2026-07-10 17:46:59 +09:00
parent 0d0c51844a
commit dfd9615942
10 changed files with 58 additions and 104 deletions

Binary file not shown.

View File

@@ -96,13 +96,26 @@ public class McpBridge {
int idx = line.indexOf("\"id\":"); int idx = line.indexOf("\"id\":");
if (idx == -1) return "null"; if (idx == -1) return "null";
int start = idx + 5; int start = idx + 5;
while (start < line.length() && (line.charAt(start) == ' ' || line.charAt(start) == '\"')) { while (start < line.length() && line.charAt(start) == ' ') {
start++; start++;
} }
if (start >= line.length()) return "null";
int end = start; int end = start;
while (end < line.length() && Character.isDigit(line.charAt(end))) { if (line.charAt(start) == '\"') {
end++; end = start + 1;
while (end < line.length() && line.charAt(end) != '\"') {
end++;
}
if (end < line.length()) {
end++; // include closing quote
}
} else {
while (end < line.length() && Character.isDigit(line.charAt(end))) {
end++;
}
} }
if (start == end) return "null"; if (start == end) return "null";
return line.substring(start, end); return line.substring(start, end);
} }

View File

@@ -123,9 +123,16 @@ public class BusinessToolController {
} }
} }
log.error("[Tool] 실행할 함수(Method)를 찾을 수 없습니다: {}. 현재 스캔된 툴 메서드 목록: {}", functionName, availableFunctions); log.error("[Tool] 실행할 함수(Method)를 찾을 수 없습니다: {}. 현재 스캔된 툴 메서드 목록: {}", functionName, availableFunctions);
Map<String, Object> errorDetails = new HashMap<>();
errorDetails.put("status", "404");
Map<String, Object> errorBody = new HashMap<>();
errorBody.put("code", "TOOL_NOT_FOUND");
errorBody.put("message", "실행할 함수를 찾을 수 없습니다: " + functionName);
errorBody.put("details", errorDetails);
Map<String, Object> error = new HashMap<>(); Map<String, Object> error = new HashMap<>();
error.put("jsonrpc", "2.0"); error.put("jsonrpc", "2.0");
error.put("error", Map.of("code", -32601, "message", "실행할 함수를 찾을 수 없습니다: " + functionName)); error.put("error", errorBody);
error.put("id", payload != null ? payload.get("id") : null); error.put("id", payload != null ? payload.get("id") : null);
return error; return error;
} }
@@ -145,13 +152,20 @@ public class BusinessToolController {
Set<ValidationMessage> errors = schema.validate(objectMapper.valueToTree(arguments)); Set<ValidationMessage> errors = schema.validate(objectMapper.valueToTree(arguments));
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors); log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors);
Map<String, Object> error = new HashMap<>();
error.put("jsonrpc", "2.0");
List<String> errorMessages = new ArrayList<>(); List<String> errorMessages = new ArrayList<>();
for (ValidationMessage vm : errors) { for (ValidationMessage vm : errors) {
errorMessages.add(vm.getMessage()); errorMessages.add(vm.getMessage());
} }
error.put("error", Map.of("code", -32602, "message", "파라미터 유효성 검증 실패: " + String.join(", ", errorMessages))); Map<String, Object> errorDetails = new HashMap<>();
errorDetails.put("status", "422");
Map<String, Object> errorBody = new HashMap<>();
errorBody.put("code", "INVALID_PARAM");
errorBody.put("message", "파라미터 유효성 검증 실패: " + String.join(", ", errorMessages));
errorBody.put("details", errorDetails);
Map<String, Object> error = new HashMap<>();
error.put("jsonrpc", "2.0");
error.put("error", errorBody);
error.put("id", payload != null ? payload.get("id") : null); error.put("id", payload != null ? payload.get("id") : null);
return error; return error;
} }
@@ -177,15 +191,23 @@ public class BusinessToolController {
// 4. 메서드 실행 // 4. 메서드 실행
Object methodResult = targetMethod.invoke(targetBean, invokeArgument); Object methodResult = targetMethod.invoke(targetBean, invokeArgument);
// 5. 결과 조립 (JSON-RPC 응답) // 5. 결과 조립 (JSON-RPC 응답 - Agent Builder 규격 적용)
Map<String, Object> resultPayload = new HashMap<>(); Map<String, Object> innerResult = new HashMap<>();
if (methodResult instanceof Map) { if (methodResult instanceof Map) {
resultPayload = (Map<String, Object>) methodResult; innerResult.putAll((Map<String, Object>) methodResult);
} else {
resultPayload.put("status", "SUCCESS");
resultPayload.put("legacy_response", methodResult);
} }
resultPayload.put("ai_insight", "다이렉트 메서드(" + targetMethod.getName() + ") 통신이 성공적으로 수행되었습니다."); if (!innerResult.containsKey("contracts")) {
List<Object> contracts = new ArrayList<>();
if (methodResult != null) {
contracts.add(methodResult);
}
innerResult.put("contracts", contracts);
}
Map<String, Object> resultPayload = new HashMap<>();
resultPayload.put("status", "ok");
resultPayload.put("result", innerResult);
resultPayload.put("error code", null);
Map<String, Object> rpcResponse = new java.util.LinkedHashMap<>(); // 순서 보장을 위해 LinkedHashMap 사용 Map<String, Object> rpcResponse = new java.util.LinkedHashMap<>(); // 순서 보장을 위해 LinkedHashMap 사용
rpcResponse.put("jsonrpc", "2.0"); rpcResponse.put("jsonrpc", "2.0");
@@ -201,9 +223,16 @@ public class BusinessToolController {
} catch (Exception e) { } catch (Exception e) {
log.error("[Tool] 리플렉션 실행 중 예외 발생: {}", e.getMessage()); log.error("[Tool] 리플렉션 실행 중 예외 발생: {}", e.getMessage());
Map<String, Object> errorDetails = new HashMap<>();
errorDetails.put("status", "500");
Map<String, Object> errorBody = new HashMap<>();
errorBody.put("code", "TOOL_ERROR");
errorBody.put("message", "메서드 실행 중 예외 발생: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()));
errorBody.put("details", errorDetails);
Map<String, Object> error = new HashMap<>(); Map<String, Object> error = new HashMap<>();
error.put("jsonrpc", "2.0"); error.put("jsonrpc", "2.0");
error.put("error", Map.of("code", -32000, "message", "메서드 실행 중 예외 발생: " + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()))); error.put("error", errorBody);
error.put("id", payload != null ? payload.get("id") : null); error.put("id", payload != null ? payload.get("id") : null);
return error; return error;
} }

View File

@@ -1,4 +0,0 @@
FROM eclipse-temurin:21-jdk-alpine
WORKDIR /app
COPY build/libs/axhub-tool-hr-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -1,10 +0,0 @@
plugins {
id 'org.springframework.boot'
}
dependencies {
implementation project(':axhub-tool-core')
}
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
}

View File

@@ -1,28 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.tool.hr;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
/**
* @package io.shinhanlife.axhub.biz.mcp.tool.hr
* @className HrToolApplication
* @description AX HUB 시스템 처리 클래스
* @author
* @create
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 최초생성
*
* </pre>
*/
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@org.springframework.boot.context.properties.ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@EnableCaching
public class HrToolApplication {
public static void main(String[] args) {
SpringApplication.run(HrToolApplication.class, args);
}
}

View File

@@ -1,32 +0,0 @@
# Local 환경 전용 설정 (H2 메모리 DB 등)
spring.datasource.url=jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1;
spring.datasource.driverClassName=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.username=sa
spring.datasource.password=password
spring.h2.console.enabled=true
# EIMS 동적 라우팅 접속 정보
eims.http.url=http://localhost:${server.port}/api/gateway
eims.tcp.host=127.0.0.1
eims.tcp.port=8090
eims.tcp.timeout=5000
eims.jsp.form.url=http://localhost:${server.port}/mock/jsp-form
eims.jsp.json.url=http://localhost:${server.port}/mock/jsp-json
eims.mci.url=http://localhost:${server.port}/api/mock/esb/api
eims.mcistring.url=http://localhost:${server.port}/api/mock/esb/string
# API 보안 키 설정
mcp.security.api-keys.SHINHAN_MCP_SECRET_KEY_2026=mcp-client-1
mcp.security.api-keys.SHINHAN_MCP_TEST_KEY_9999=mcp-client-2
mcp.security.tenant-domains.mcp-client-1=CUSTOMER,COMMON
mcp.security.tenant-domains.mcp-client-2=ALL
# Gateway/Tool URLs
axhub.gateway.url=http://localhost:8081
axhub.tool.url=http://localhost:${server.port}
# Disable Kafka
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration
# Suppress Kafka Connection Logs
logging.level.org.apache.kafka=ERROR

View File

@@ -1,12 +0,0 @@
spring:
application:
name: axhub-tool-hr
config:
import: "classpath:config/application-glow-local.yml"
server:
port: 8086
axhub:
tool:
url: "http://tool-hr:8086"

View File

@@ -1,2 +0,0 @@
spring.profiles.active=local
mcp.namespace=hr

View File

@@ -30,7 +30,7 @@ public class BondIssueService extends AbstractMcpToolService {
return executeLegacy("EAI", "BOND_001", data); return executeLegacy("EAI", "BOND_001", data);
} }
@McpFunction(name = "issue", register = true, description = "증권 발행 테스트", prompt = "디지털 증권 발행 프로세스를 실행해.", mappingId = "BOND_002") 1 @McpFunction(name = "issue", register = true, description = "증권 발행 테스트1", prompt = "디지털 증권 발행 프로세스를 실행해.", mappingId = "BOND_002")
public Object issue(BondIssueReq data) { public Object issue(BondIssueReq data) {
return executeLegacy("EAI", "BOND_002", data); return executeLegacy("EAI", "BOND_002", data);
} }