Add configurable MCP payload tracing and external tool domains
All checks were successful
Deploy Gateway / deploy (push) Successful in 1m48s
All checks were successful
Deploy Gateway / deploy (push) Successful in 1m48s
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
{
|
||||
"status": "ACTIVE",
|
||||
"serviceKey": "was-cus",
|
||||
"serviceDomain": "http://was-cus:8084",
|
||||
"serviceDomain": "https://tool-cus.devjun.net",
|
||||
"manifestPath": "/tool-manifest",
|
||||
"executeBasePath": "/mcp"
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
{
|
||||
"status": "ACTIVE",
|
||||
"serviceKey": "was-sal",
|
||||
"serviceDomain": "http://was-sal:8082",
|
||||
"serviceDomain": "https://tool-sal.devjun.net",
|
||||
"manifestPath": "/tool-manifest",
|
||||
"executeBasePath": "/mcp"
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
{
|
||||
"status": "ACTIVE",
|
||||
"serviceKey": "was-pro",
|
||||
"serviceDomain": "http://was-pro:8085",
|
||||
"serviceDomain": "https://tool-pro.devjun.net",
|
||||
"manifestPath": "/tool-manifest",
|
||||
"executeBasePath": "/mcp"
|
||||
}
|
||||
@@ -42,7 +42,7 @@
|
||||
{
|
||||
"status": "ACTIVE",
|
||||
"serviceKey": "was-sys",
|
||||
"serviceDomain": "http://was-sys:8086",
|
||||
"serviceDomain": "https://tool-sys.devjun.net",
|
||||
"manifestPath": "/tool-manifest",
|
||||
"executeBasePath": "/mcp"
|
||||
}
|
||||
|
||||
@@ -223,3 +223,6 @@ Portal Registry를 사용하는 구성에서는 포털을 route별 Tool Server e
|
||||
## Tool list change notification
|
||||
|
||||
`initialize`는 `capabilities.tools.listChanged=true`를 선언한다. 배경 Registry refresh가 기존 route snapshot과 다른 Tool 목록을 성공적으로 확보하면 `ToolListChangedEvent`가 표준 `notifications/tools/list_changed` JSON-RPC notification envelope를 만든다. 현재 HTTP 단발 응답 transport는 notification을 직접 push하지 않으며, SSE/Streamable HTTP 전송 계층이 추가되면 이 이벤트를 route별 Agent 연결에 전달하고 Agent Builder가 `tools/list`를 다시 호출한다.
|
||||
|
||||
|
||||
- 임시 검증에서 Agent↔MCP와 MCP↔Tool Service payload를 확인해야 하면 `mcp.trace.payload-logging-enabled=true`를 켠다. 이 로그는 JSON 한 줄 형태로 요청·응답 본문을 남기므로 운영 기본값은 false이며, 검증 후 즉시 꺼야 한다.
|
||||
|
||||
@@ -187,9 +187,14 @@ public record McpProperties(
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP 경계 로그 활성화와 수신 요청 최대 크기 정책 설정입니다.
|
||||
* MCP 경계 로그와 선택적인 payload logging 정책 설정입니다.
|
||||
* payload logging은 Agent·Tool 요청/응답 본문을 남기므로 로컬 검증처럼 명시적으로 켠 환경에서만 사용합니다.
|
||||
*/
|
||||
public record Trace(boolean enabled, @Min(1) int maxBodyBytes) {
|
||||
public record Trace(
|
||||
boolean enabled,
|
||||
@Min(1) int maxBodyBytes,
|
||||
boolean payloadLoggingEnabled,
|
||||
@Min(1) int maxPayloadBytes) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,34 +1,44 @@
|
||||
package io.shinhanlife.dap.biz.mcp.observability;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.NullNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.node.TextNode;
|
||||
import io.shinhanlife.dap.biz.mcp.config.McpProperties;
|
||||
import io.shinhanlife.dap.biz.mcp.context.McpRequestContext;
|
||||
import io.shinhanlife.dap.biz.mcp.context.McpRequestContextHolder;
|
||||
|
||||
import java.util.StringJoiner;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* MCP HTTP 입출구와 Tool HTTP 호출 경계의 이벤트를 한 줄 key=value 로그로 남깁니다. 현재 요청의 guid와 requestId는 {@link McpRequestContextHolder}에서 읽어 로그 메시지에 직접 포함하므로 MDC를 사용하지 않습니다.
|
||||
* payload, credential, 사원 식별자({@code employeeNo}·{@code virtualEmployeeNo})는 기록하지 않습니다. 주요 의존성은 로그 활성화 정책을 제공하는 {@link McpProperties}와 SLF4J입니다.
|
||||
* MCP HTTP 입출구와 Tool HTTP 호출 경계의 이벤트를 로그로 남기는 관측 컴포넌트입니다.
|
||||
* 일반 trace는 key=value 형식으로 유지하고, payload 로그는 명시적으로 켜진 검증 환경에서만 JSON 한 줄로 남깁니다.
|
||||
* 요청 본문과 Tool 응답에는 업무 데이터가 포함될 수 있으므로 기본값은 비활성화이며, 주요 의존성은 trace 설정을 가진 {@link McpProperties}와 JSON 직렬화를 담당하는 {@link ObjectMapper}입니다.
|
||||
*/
|
||||
@Component
|
||||
public class TraceLogger {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TraceLogger.class);
|
||||
|
||||
private final McpProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* trace 로그 활성화 여부를 판단할 설정 객체를 주입받습니다.
|
||||
* trace 설정과 payload JSON 직렬화기를 주입받습니다.
|
||||
* payload logging 활성 여부와 최대 기록 크기는 {@code mcp.trace.*} 설정으로 판단합니다.
|
||||
*/
|
||||
public TraceLogger(McpProperties properties) {
|
||||
public TraceLogger(McpProperties properties, ObjectMapper objectMapper) {
|
||||
this.properties = properties;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 정상적인 처리 단계를 key=value 형식의 구조화 로그로 남깁니다. trace 로그 설정이 켜진 경우에만 기록합니다.
|
||||
* 정상적인 처리 단계를 key=value 형식의 구조화 로그로 남깁니다.
|
||||
* payload는 포함하지 않으며, trace 로그 설정이 켜진 경우에만 기록합니다.
|
||||
*/
|
||||
public void event(String event, Object... keyValues) {
|
||||
if (properties.trace().enabled()) {
|
||||
@@ -43,7 +53,31 @@ public class TraceLogger {
|
||||
}
|
||||
|
||||
/**
|
||||
* 예외가 발생한 처리 단계를 오류 로그로 남깁니다. 오류 로그에는 예외 종류와 메시지를 함께 남겨 원인 분석을 돕습니다.
|
||||
* Agent와 MCP, MCP와 Tool Server 사이의 요청·응답 payload를 JSON 한 줄 로그로 남깁니다.
|
||||
* 업무 payload 노출 위험 때문에 {@code mcp.trace.payload-logging-enabled=true}인 경우에만 동작하고, 설정된 byte 상한을 넘으면 본문 대신 축약 메시지를 남깁니다.
|
||||
*/
|
||||
public void payload(String event, Object payload, Object... keyValues) {
|
||||
if (!properties.trace().enabled() || !properties.trace().payloadLoggingEnabled()) {
|
||||
return;
|
||||
}
|
||||
McpRequestContext context = McpRequestContextHolder.get().orElse(null);
|
||||
ObjectNode envelope = objectMapper.createObjectNode();
|
||||
envelope.put("event", safe(event));
|
||||
envelope.put("guid", context == null ? "" : context.guid());
|
||||
envelope.put("requestId", context == null ? "" : context.requestId());
|
||||
envelope.put("routeKey", context == null ? "" : context.routeKey());
|
||||
addFields(envelope, keyValues);
|
||||
envelope.set("payload", boundedPayload(payload));
|
||||
try {
|
||||
log.info(objectMapper.writeValueAsString(envelope));
|
||||
} catch (JsonProcessingException exception) {
|
||||
log.warn("event={} payloadLoggingFailed=true reason={}", safe(event), exception.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 예외가 발생한 처리 단계를 오류 로그로 남깁니다.
|
||||
* 오류 로그에도 payload는 포함하지 않고, 예외 종류와 메시지만 correlation 값과 함께 남깁니다.
|
||||
*/
|
||||
public void error(String event, Throwable error, Object... keyValues) {
|
||||
McpRequestContext context = McpRequestContextHolder.get().orElse(null);
|
||||
@@ -59,7 +93,8 @@ public class TraceLogger {
|
||||
}
|
||||
|
||||
/**
|
||||
* 가변 인자로 받은 키와 값을 두 개씩 묶어 읽기 쉬운 key=value 문자열로 바꿉니다. 홀수 개가 들어오면 짝이 없는 마지막 값은 기록하지 않습니다.
|
||||
* 가변 인자로 받은 key/value 쌍을 사람이 읽기 쉬운 key=value 문자열로 변환합니다.
|
||||
* 홀수 개가 들어오면 짝이 없는 마지막 값은 기록하지 않습니다.
|
||||
*/
|
||||
private String fields(Object... keyValues) {
|
||||
StringJoiner joiner = new StringJoiner(" ");
|
||||
@@ -70,7 +105,60 @@ public class TraceLogger {
|
||||
}
|
||||
|
||||
/**
|
||||
* 줄바꿈과 공백을 치환해 한 로그 이벤트가 여러 줄로 갈라지지 않도록 문자열을 정리합니다.
|
||||
* payload 로그 envelope에 추가 필드를 안전한 JSON 값으로 넣습니다.
|
||||
* 필드명은 로그 검색에 쓰이는 메타데이터만 받는 전제라 개행과 공백을 정리합니다.
|
||||
*/
|
||||
private void addFields(ObjectNode envelope, Object... keyValues) {
|
||||
for (int index = 0; index + 1 < keyValues.length; index += 2) {
|
||||
String key = safe(keyValues[index]);
|
||||
Object value = keyValues[index + 1];
|
||||
if (!key.isBlank()) {
|
||||
envelope.set(key, objectMapper.valueToTree(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* payload를 JSON 노드로 바꾸고 설정된 최대 byte 수를 넘으면 축약 문자열로 대체합니다.
|
||||
* 문자열 payload가 JSON이면 JSON으로 파싱하고, 그렇지 않으면 문자열 그대로 기록합니다.
|
||||
*/
|
||||
private JsonNode boundedPayload(Object payload) {
|
||||
JsonNode node = payloadNode(payload);
|
||||
try {
|
||||
String serialized = objectMapper.writeValueAsString(node);
|
||||
if (serialized.getBytes(java.nio.charset.StandardCharsets.UTF_8).length
|
||||
> properties.trace().maxPayloadBytes()) {
|
||||
return TextNode.valueOf("<payload omitted: exceeds maxPayloadBytes>");
|
||||
}
|
||||
return node;
|
||||
} catch (JsonProcessingException exception) {
|
||||
return TextNode.valueOf("<payload omitted: serialization failed>");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 다양한 payload 입력을 JSON 노드로 정규화합니다.
|
||||
* 이미 JsonNode면 그대로 쓰고, 문자열은 JSON 파싱을 먼저 시도한 뒤 실패하면 text node로 보관합니다.
|
||||
*/
|
||||
private JsonNode payloadNode(Object payload) {
|
||||
if (payload == null) {
|
||||
return NullNode.getInstance();
|
||||
}
|
||||
if (payload instanceof JsonNode jsonNode) {
|
||||
return jsonNode;
|
||||
}
|
||||
if (payload instanceof String text) {
|
||||
try {
|
||||
return objectMapper.readTree(text);
|
||||
} catch (Exception ignored) {
|
||||
return TextNode.valueOf(text);
|
||||
}
|
||||
}
|
||||
return objectMapper.valueToTree(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 줄바꿈과 공백을 치환해 로그 이벤트가 여러 줄로 갈라지지 않게 합니다.
|
||||
*/
|
||||
private String safe(Object value) {
|
||||
if (value == null) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.TextNode;
|
||||
import io.shinhanlife.dap.biz.mcp.config.McpProperties;
|
||||
import io.shinhanlife.dap.biz.mcp.context.McpRequestContext;
|
||||
import io.shinhanlife.dap.biz.mcp.observability.TraceLogger;
|
||||
import io.shinhanlife.dap.biz.mcp.toolclient.ToolClient.ToolClientException;
|
||||
import io.shinhanlife.dap.biz.mcp.toolclient.ToolClient.ToolRequest;
|
||||
import io.shinhanlife.dap.biz.mcp.toolclient.ToolClient.ToolResponse;
|
||||
@@ -30,6 +31,7 @@ public class HttpToolClient implements ToolClient {
|
||||
private final ObjectMapper objectMapper;
|
||||
private final McpProperties properties;
|
||||
private final HttpClient toolHttpClient;
|
||||
private final TraceLogger traceLogger;
|
||||
|
||||
/**
|
||||
* JSON 변환, Tool 설정, 공유 connection pool을 가진 HTTP client를 주입받습니다.
|
||||
@@ -37,10 +39,12 @@ public class HttpToolClient implements ToolClient {
|
||||
public HttpToolClient(
|
||||
ObjectMapper objectMapper,
|
||||
McpProperties properties,
|
||||
@Qualifier("toolHttpClient") HttpClient toolHttpClient) {
|
||||
@Qualifier("toolHttpClient") HttpClient toolHttpClient,
|
||||
TraceLogger traceLogger) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.properties = properties;
|
||||
this.toolHttpClient = toolHttpClient;
|
||||
this.traceLogger = traceLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,6 +53,17 @@ public class HttpToolClient implements ToolClient {
|
||||
@Override
|
||||
public ToolResponse execute(ToolRequest request, McpRequestContext context) {
|
||||
try {
|
||||
traceLogger.payload(
|
||||
"tool_http_request_body",
|
||||
request.arguments(),
|
||||
"direction",
|
||||
"mcp_to_tool_server",
|
||||
"toolName",
|
||||
request.toolName(),
|
||||
"version",
|
||||
request.version(),
|
||||
"endpoint",
|
||||
request.endpoint());
|
||||
RestClient.RequestBodySpec spec = requestSpec(request, context);
|
||||
var entity =
|
||||
spec.retrieve()
|
||||
@@ -58,9 +73,19 @@ public class HttpToolClient implements ToolClient {
|
||||
throw statusException(response.getStatusCode().value(), request.toolName());
|
||||
})
|
||||
.toEntity(String.class);
|
||||
return new ToolResponse(
|
||||
entity.getStatusCode().value(),
|
||||
parseResponse(entity.getBody(), entity.getHeaders().getContentType()));
|
||||
JsonNode responseBody = parseResponse(entity.getBody(), entity.getHeaders().getContentType());
|
||||
traceLogger.payload(
|
||||
"tool_http_response_body",
|
||||
responseBody,
|
||||
"direction",
|
||||
"tool_server_to_mcp",
|
||||
"toolName",
|
||||
request.toolName(),
|
||||
"version",
|
||||
request.version(),
|
||||
"statusCode",
|
||||
entity.getStatusCode().value());
|
||||
return new ToolResponse(entity.getStatusCode().value(), responseBody);
|
||||
} catch (ToolClientException exception) {
|
||||
throw exception;
|
||||
} catch (ResourceAccessException exception) {
|
||||
|
||||
@@ -84,6 +84,15 @@ final class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
|
||||
/**
|
||||
* 요청의 문자 인코딩에 맞는 Reader를 반환합니다. 문자 인코딩이 없으면 JSON의 기본 인코딩인 UTF-8을 사용합니다.
|
||||
*/
|
||||
/**
|
||||
* 캐시된 요청 본문을 payload 로그용 문자열로 반환합니다.
|
||||
* 이미 메모리에 보관된 byte 배열만 읽으므로 controller가 다시 본문을 읽는 흐름에는 영향을 주지 않습니다.
|
||||
*/
|
||||
String bodyText() {
|
||||
String encoding = getCharacterEncoding();
|
||||
Charset charset = encoding == null ? StandardCharsets.UTF_8 : Charset.forName(encoding);
|
||||
return new String(body, charset);
|
||||
}
|
||||
@Override
|
||||
public BufferedReader getReader() {
|
||||
String encoding = getCharacterEncoding();
|
||||
|
||||
@@ -9,6 +9,7 @@ import io.shinhanlife.dap.biz.mcp.jsonrpc.JsonRpcRequest;
|
||||
import io.shinhanlife.dap.biz.mcp.jsonrpc.JsonRpcRequestParser;
|
||||
import io.shinhanlife.dap.biz.mcp.jsonrpc.JsonRpcResponse;
|
||||
import io.shinhanlife.dap.biz.mcp.method.McpMethodHandlerRegistry;
|
||||
import io.shinhanlife.dap.biz.mcp.observability.TraceLogger;
|
||||
import java.util.UUID;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -27,14 +28,16 @@ public class McpController {
|
||||
|
||||
private final JsonRpcRequestParser requestParser;
|
||||
private final McpMethodHandlerRegistry handlerRegistry;
|
||||
private final TraceLogger traceLogger;
|
||||
|
||||
/**
|
||||
* JSON-RPC 변환과 method dispatch 협력 객체를 주입받습니다. Controller는 실행 규칙을 직접 구현하지 않고 각 책임 객체를 올바른 순서로 연결합니다.
|
||||
*/
|
||||
public McpController(
|
||||
JsonRpcRequestParser requestParser, McpMethodHandlerRegistry handlerRegistry) {
|
||||
JsonRpcRequestParser requestParser, McpMethodHandlerRegistry handlerRegistry, TraceLogger traceLogger) {
|
||||
this.requestParser = requestParser;
|
||||
this.handlerRegistry = handlerRegistry;
|
||||
this.traceLogger = traceLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,6 +55,15 @@ public class McpController {
|
||||
try {
|
||||
JsonRpcResponse response = handlerRegistry.resolve(request.method()).handle(request, context);
|
||||
if (request.notification()) {
|
||||
traceLogger.payload(
|
||||
"mcp_http_response_body",
|
||||
null,
|
||||
"direction",
|
||||
"mcp_to_agent",
|
||||
"mcpMethod",
|
||||
request.method(),
|
||||
"httpStatus",
|
||||
202);
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
ResponseEntity.BodyBuilder responseBuilder =
|
||||
@@ -59,6 +71,15 @@ public class McpController {
|
||||
if ("initialize".equals(request.method())) {
|
||||
responseBuilder.header(MCP_SESSION_ID_HEADER, UUID.randomUUID().toString());
|
||||
}
|
||||
traceLogger.payload(
|
||||
"mcp_http_response_body",
|
||||
response,
|
||||
"direction",
|
||||
"mcp_to_agent",
|
||||
"mcpMethod",
|
||||
request.method(),
|
||||
"httpStatus",
|
||||
200);
|
||||
return responseBuilder.body(response);
|
||||
} catch (JsonRpcException exception) {
|
||||
if (exception.requestId() != null) {
|
||||
|
||||
@@ -104,6 +104,17 @@ public class McpExchangeFilter extends OncePerRequestFilter {
|
||||
request.getRequestURI(),
|
||||
"mcpMethod",
|
||||
mcpMethod);
|
||||
traceLogger.payload(
|
||||
"mcp_http_request_body",
|
||||
cachedRequest.bodyText(),
|
||||
"direction",
|
||||
"agent_to_mcp",
|
||||
"httpMethod",
|
||||
request.getMethod(),
|
||||
"path",
|
||||
request.getRequestURI(),
|
||||
"mcpMethod",
|
||||
mcpMethod);
|
||||
|
||||
try {
|
||||
protocolVersionValidator.validatePostInitializeRequest(request, mcpMethod);
|
||||
|
||||
@@ -94,6 +94,9 @@ mcp:
|
||||
bundles: []
|
||||
trace:
|
||||
enabled: true
|
||||
# Request/response body logging is for temporary verification only. Keep disabled by default.
|
||||
payload-logging-enabled: ${MCP_TRACE_PAYLOAD_LOGGING_ENABLED:true}
|
||||
max-payload-bytes: ${MCP_TRACE_MAX_PAYLOAD_BYTES:65536}
|
||||
# Rejects oversized MCP request bodies before controller processing.
|
||||
max-body-bytes: 1048576
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ public final class TestFixtures {
|
||||
"file:./config/local-core-tools-manifest-sample-v1.json", 30, 5),
|
||||
new McpProperties.ToolClient(1_000, 5_000, 300_000, forwardAuthorization, "tool-server-key"),
|
||||
new McpProperties.Redis(redisEnabled, "test:mcp:tools", "test:mcp:portal-registry"),
|
||||
new McpProperties.Trace(true, 1_048_576),
|
||||
new McpProperties.Trace(true, 1_048_576, false, 65_536),
|
||||
new McpProperties.Protocol(List.of("2025-11-25"), "2025-11-25"),
|
||||
new McpProperties.Discovery(!bundles.isEmpty(), 1_000, 3_000, 100, 200, 1_048_576, 30_000),
|
||||
new McpProperties.Portal(false, "", "", 300),
|
||||
|
||||
@@ -26,7 +26,7 @@ class TraceLoggerTest {
|
||||
logger.addAppender(appender);
|
||||
McpRequestContextHolder.set(context());
|
||||
|
||||
new TraceLogger(properties(false, false))
|
||||
new TraceLogger(properties(false, false), io.shinhanlife.dap.biz.mcp.TestFixtures.OBJECT_MAPPER)
|
||||
.event("mcp_http_response_completed", "httpStatus", 200);
|
||||
|
||||
assertThat(appender.list)
|
||||
|
||||
@@ -41,7 +41,7 @@ class HttpToolClientTest {
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody("{\"customerName\":\"홍길동\"}"));
|
||||
HttpToolClient client =
|
||||
new HttpToolClient(OBJECT_MAPPER, properties(false, false), HttpClient.newHttpClient());
|
||||
new HttpToolClient(OBJECT_MAPPER, properties(false, false), HttpClient.newHttpClient(), new io.shinhanlife.dap.biz.mcp.observability.TraceLogger(properties(false, false), OBJECT_MAPPER));
|
||||
ToolRequest request =
|
||||
new ToolRequest(
|
||||
"customer.search",
|
||||
@@ -75,7 +75,7 @@ class HttpToolClientTest {
|
||||
server.enqueue(new MockResponse().setHeader("Content-Type", "application/json")
|
||||
.setBody("{\"quote\":\"시작이 반이다.\"}"));
|
||||
HttpToolClient client =
|
||||
new HttpToolClient(OBJECT_MAPPER, properties(false, false), HttpClient.newHttpClient());
|
||||
new HttpToolClient(OBJECT_MAPPER, properties(false, false), HttpClient.newHttpClient(), new io.shinhanlife.dap.biz.mcp.observability.TraceLogger(properties(false, false), OBJECT_MAPPER));
|
||||
ToolRequest request = new ToolRequest(
|
||||
"smp_quote_daily", "1.0.0", server.url("/mcp/smp_quote_daily").toString(),
|
||||
OBJECT_MAPPER.readTree("{\"category\":\"속담\"}"), 3_000);
|
||||
@@ -97,7 +97,7 @@ class HttpToolClientTest {
|
||||
void preservesPlainTextToolResponseAsTextNode() throws Exception {
|
||||
server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("123"));
|
||||
HttpToolClient client =
|
||||
new HttpToolClient(OBJECT_MAPPER, properties(false, false), HttpClient.newHttpClient());
|
||||
new HttpToolClient(OBJECT_MAPPER, properties(false, false), HttpClient.newHttpClient(), new io.shinhanlife.dap.biz.mcp.observability.TraceLogger(properties(false, false), OBJECT_MAPPER));
|
||||
ToolRequest request =
|
||||
new ToolRequest(
|
||||
"processing",
|
||||
@@ -116,7 +116,7 @@ class HttpToolClientTest {
|
||||
void preservesHttpStatusOnToolError() throws Exception {
|
||||
server.enqueue(new MockResponse().setResponseCode(410).setBody("gone"));
|
||||
HttpToolClient client =
|
||||
new HttpToolClient(OBJECT_MAPPER, properties(false, false), HttpClient.newHttpClient());
|
||||
new HttpToolClient(OBJECT_MAPPER, properties(false, false), HttpClient.newHttpClient(), new io.shinhanlife.dap.biz.mcp.observability.TraceLogger(properties(false, false), OBJECT_MAPPER));
|
||||
ToolRequest request =
|
||||
new ToolRequest(
|
||||
"customer.search",
|
||||
|
||||
@@ -41,7 +41,7 @@ class McpControllerTest {
|
||||
McpRequestContextHolder.set(context());
|
||||
|
||||
var response =
|
||||
new McpController(parser, registry).handleMcpRequest(JsonNodeFactory.instance.objectNode());
|
||||
new McpController(parser, registry, org.mockito.Mockito.mock(io.shinhanlife.dap.biz.mcp.observability.TraceLogger.class)).handleMcpRequest(JsonNodeFactory.instance.objectNode());
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
|
||||
assertThat(response.getBody()).isNull();
|
||||
@@ -64,7 +64,7 @@ class McpControllerTest {
|
||||
McpRequestContextHolder.set(context());
|
||||
|
||||
var response =
|
||||
new McpController(parser, registry).handleMcpRequest(JsonNodeFactory.instance.objectNode());
|
||||
new McpController(parser, registry, org.mockito.Mockito.mock(io.shinhanlife.dap.biz.mcp.observability.TraceLogger.class)).handleMcpRequest(JsonNodeFactory.instance.objectNode());
|
||||
|
||||
String sessionId = response.getHeaders().getFirst(McpController.MCP_SESSION_ID_HEADER);
|
||||
assertThat(sessionId).isNotBlank();
|
||||
@@ -89,7 +89,7 @@ class McpControllerTest {
|
||||
McpRequestContextHolder.set(context());
|
||||
|
||||
var response =
|
||||
new McpController(parser, registry).handleMcpRequest(JsonNodeFactory.instance.objectNode());
|
||||
new McpController(parser, registry, org.mockito.Mockito.mock(io.shinhanlife.dap.biz.mcp.observability.TraceLogger.class)).handleMcpRequest(JsonNodeFactory.instance.objectNode());
|
||||
|
||||
PostMapping mapping =
|
||||
McpController.class
|
||||
|
||||
@@ -219,7 +219,7 @@ class McpExchangeFilterTest {
|
||||
base.registry(),
|
||||
base.toolClient(),
|
||||
base.redis(),
|
||||
new McpProperties.Trace(true, 8),
|
||||
new McpProperties.Trace(true, 8, false, 65_536),
|
||||
base.protocol(),
|
||||
base.discovery(),
|
||||
base.portal(),
|
||||
@@ -357,7 +357,7 @@ class McpExchangeFilterTest {
|
||||
private McpExchangeFilter filter(McpProperties properties) {
|
||||
return new McpExchangeFilter(
|
||||
new McpRequestContextFactory(properties),
|
||||
new TraceLogger(properties),
|
||||
new TraceLogger(properties, io.shinhanlife.dap.biz.mcp.TestFixtures.OBJECT_MAPPER),
|
||||
OBJECT_MAPPER,
|
||||
properties,
|
||||
new McpProtocolVersionValidator(properties));
|
||||
|
||||
Reference in New Issue
Block a user