diff --git a/deploy/portal-registry.json b/deploy/portal-registry.json index 1dda7c0..be224c7 100644 --- a/deploy/portal-registry.json +++ b/deploy/portal-registry.json @@ -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" } diff --git a/docs/architecture.md b/docs/architecture.md index 8a2f5bc..239f514 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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이며, 검증 후 즉시 꺼야 한다. diff --git a/src/main/java/io/shinhanlife/dap/biz/mcp/config/McpProperties.java b/src/main/java/io/shinhanlife/dap/biz/mcp/config/McpProperties.java index f25df00..ee8512a 100644 --- a/src/main/java/io/shinhanlife/dap/biz/mcp/config/McpProperties.java +++ b/src/main/java/io/shinhanlife/dap/biz/mcp/config/McpProperties.java @@ -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) { } /** diff --git a/src/main/java/io/shinhanlife/dap/biz/mcp/observability/TraceLogger.java b/src/main/java/io/shinhanlife/dap/biz/mcp/observability/TraceLogger.java index f7af42f..c0dcf68 100644 --- a/src/main/java/io/shinhanlife/dap/biz/mcp/observability/TraceLogger.java +++ b/src/main/java/io/shinhanlife/dap/biz/mcp/observability/TraceLogger.java @@ -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(""); + } + return node; + } catch (JsonProcessingException exception) { + return TextNode.valueOf(""); + } + } + + /** + * 다양한 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) { diff --git a/src/main/java/io/shinhanlife/dap/biz/mcp/toolclient/HttpToolClient.java b/src/main/java/io/shinhanlife/dap/biz/mcp/toolclient/HttpToolClient.java index 7be5c2a..8d68cc2 100644 --- a/src/main/java/io/shinhanlife/dap/biz/mcp/toolclient/HttpToolClient.java +++ b/src/main/java/io/shinhanlife/dap/biz/mcp/toolclient/HttpToolClient.java @@ -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) { diff --git a/src/main/java/io/shinhanlife/dap/biz/mcp/transport/http/CachedBodyHttpServletRequest.java b/src/main/java/io/shinhanlife/dap/biz/mcp/transport/http/CachedBodyHttpServletRequest.java index 6f84430..2ddb89d 100644 --- a/src/main/java/io/shinhanlife/dap/biz/mcp/transport/http/CachedBodyHttpServletRequest.java +++ b/src/main/java/io/shinhanlife/dap/biz/mcp/transport/http/CachedBodyHttpServletRequest.java @@ -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(); diff --git a/src/main/java/io/shinhanlife/dap/biz/mcp/transport/http/McpController.java b/src/main/java/io/shinhanlife/dap/biz/mcp/transport/http/McpController.java index 3ccd562..7a715ed 100644 --- a/src/main/java/io/shinhanlife/dap/biz/mcp/transport/http/McpController.java +++ b/src/main/java/io/shinhanlife/dap/biz/mcp/transport/http/McpController.java @@ -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) { diff --git a/src/main/java/io/shinhanlife/dap/biz/mcp/transport/http/McpExchangeFilter.java b/src/main/java/io/shinhanlife/dap/biz/mcp/transport/http/McpExchangeFilter.java index b5d7487..93c5e17 100644 --- a/src/main/java/io/shinhanlife/dap/biz/mcp/transport/http/McpExchangeFilter.java +++ b/src/main/java/io/shinhanlife/dap/biz/mcp/transport/http/McpExchangeFilter.java @@ -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); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 740a921..05f5cff 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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 diff --git a/src/test/java/io/shinhanlife/dap/biz/mcp/TestFixtures.java b/src/test/java/io/shinhanlife/dap/biz/mcp/TestFixtures.java index c587abf..702657f 100644 --- a/src/test/java/io/shinhanlife/dap/biz/mcp/TestFixtures.java +++ b/src/test/java/io/shinhanlife/dap/biz/mcp/TestFixtures.java @@ -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), diff --git a/src/test/java/io/shinhanlife/dap/biz/mcp/observability/TraceLoggerTest.java b/src/test/java/io/shinhanlife/dap/biz/mcp/observability/TraceLoggerTest.java index 9e7d760..e5fbd4a 100644 --- a/src/test/java/io/shinhanlife/dap/biz/mcp/observability/TraceLoggerTest.java +++ b/src/test/java/io/shinhanlife/dap/biz/mcp/observability/TraceLoggerTest.java @@ -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) diff --git a/src/test/java/io/shinhanlife/dap/biz/mcp/toolclient/HttpToolClientTest.java b/src/test/java/io/shinhanlife/dap/biz/mcp/toolclient/HttpToolClientTest.java index 5721a91..8f0f7a5 100644 --- a/src/test/java/io/shinhanlife/dap/biz/mcp/toolclient/HttpToolClientTest.java +++ b/src/test/java/io/shinhanlife/dap/biz/mcp/toolclient/HttpToolClientTest.java @@ -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", diff --git a/src/test/java/io/shinhanlife/dap/biz/mcp/transport/http/McpControllerTest.java b/src/test/java/io/shinhanlife/dap/biz/mcp/transport/http/McpControllerTest.java index 02e6c0a..a190d26 100644 --- a/src/test/java/io/shinhanlife/dap/biz/mcp/transport/http/McpControllerTest.java +++ b/src/test/java/io/shinhanlife/dap/biz/mcp/transport/http/McpControllerTest.java @@ -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 diff --git a/src/test/java/io/shinhanlife/dap/biz/mcp/transport/http/McpExchangeFilterTest.java b/src/test/java/io/shinhanlife/dap/biz/mcp/transport/http/McpExchangeFilterTest.java index 243665f..3dcdd4b 100644 --- a/src/test/java/io/shinhanlife/dap/biz/mcp/transport/http/McpExchangeFilterTest.java +++ b/src/test/java/io/shinhanlife/dap/biz/mcp/transport/http/McpExchangeFilterTest.java @@ -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));