Add configurable MCP payload tracing and external tool domains
All checks were successful
Deploy Gateway / deploy (push) Successful in 1m48s

This commit is contained in:
2026-08-18 15:21:32 +09:00
parent 3e0c251488
commit 4e45a8f321
14 changed files with 197 additions and 32 deletions

View File

@@ -6,7 +6,7 @@
{ {
"status": "ACTIVE", "status": "ACTIVE",
"serviceKey": "was-cus", "serviceKey": "was-cus",
"serviceDomain": "http://was-cus:8084", "serviceDomain": "https://tool-cus.devjun.net",
"manifestPath": "/tool-manifest", "manifestPath": "/tool-manifest",
"executeBasePath": "/mcp" "executeBasePath": "/mcp"
} }
@@ -18,7 +18,7 @@
{ {
"status": "ACTIVE", "status": "ACTIVE",
"serviceKey": "was-sal", "serviceKey": "was-sal",
"serviceDomain": "http://was-sal:8082", "serviceDomain": "https://tool-sal.devjun.net",
"manifestPath": "/tool-manifest", "manifestPath": "/tool-manifest",
"executeBasePath": "/mcp" "executeBasePath": "/mcp"
} }
@@ -30,7 +30,7 @@
{ {
"status": "ACTIVE", "status": "ACTIVE",
"serviceKey": "was-pro", "serviceKey": "was-pro",
"serviceDomain": "http://was-pro:8085", "serviceDomain": "https://tool-pro.devjun.net",
"manifestPath": "/tool-manifest", "manifestPath": "/tool-manifest",
"executeBasePath": "/mcp" "executeBasePath": "/mcp"
} }
@@ -42,7 +42,7 @@
{ {
"status": "ACTIVE", "status": "ACTIVE",
"serviceKey": "was-sys", "serviceKey": "was-sys",
"serviceDomain": "http://was-sys:8086", "serviceDomain": "https://tool-sys.devjun.net",
"manifestPath": "/tool-manifest", "manifestPath": "/tool-manifest",
"executeBasePath": "/mcp" "executeBasePath": "/mcp"
} }

View File

@@ -223,3 +223,6 @@ Portal Registry를 사용하는 구성에서는 포털을 route별 Tool Server e
## Tool list change notification ## 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`를 다시 호출한다. `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이며, 검증 후 즉시 꺼야 한다.

View File

@@ -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) {
} }
/** /**

View File

@@ -1,34 +1,44 @@
package io.shinhanlife.dap.biz.mcp.observability; 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.config.McpProperties;
import io.shinhanlife.dap.biz.mcp.context.McpRequestContext; import io.shinhanlife.dap.biz.mcp.context.McpRequestContext;
import io.shinhanlife.dap.biz.mcp.context.McpRequestContextHolder; import io.shinhanlife.dap.biz.mcp.context.McpRequestContextHolder;
import java.util.StringJoiner; import java.util.StringJoiner;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
* MCP HTTP 입출구와 Tool HTTP 호출 경계의 이벤트를 한 줄 key=value 로그로 남깁니다. 현재 요청의 guid와 requestId는 {@link McpRequestContextHolder}에서 읽어 로그 메시지에 직접 포함하므로 MDC를 사용하지 않습니다. * MCP HTTP 입출구와 Tool HTTP 호출 경계의 이벤트를 로그로 남기는 관측 컴포넌트입니다.
* payload, credential, 사원 식별자({@code employeeNo}·{@code virtualEmployeeNo})는 기록하지 않습니다. 주요 의존성은 로그 활성화 정책을 제공하는 {@link McpProperties}와 SLF4J입니다. * 일반 trace는 key=value 형식으로 유지하고, payload 로그는 명시적으로 켜진 검증 환경에서만 JSON 한 줄로 남깁니다.
* 요청 본문과 Tool 응답에는 업무 데이터가 포함될 수 있으므로 기본값은 비활성화이며, 주요 의존성은 trace 설정을 가진 {@link McpProperties}와 JSON 직렬화를 담당하는 {@link ObjectMapper}입니다.
*/ */
@Component @Component
public class TraceLogger { public class TraceLogger {
private static final Logger log = LoggerFactory.getLogger(TraceLogger.class); private static final Logger log = LoggerFactory.getLogger(TraceLogger.class);
private final McpProperties properties; 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.properties = properties;
this.objectMapper = objectMapper;
} }
/** /**
* 정상적인 처리 단계를 key=value 형식의 구조화 로그로 남깁니다. trace 로그 설정이 켜진 경우에만 기록합니다. * 정상적인 처리 단계를 key=value 형식의 구조화 로그로 남깁니다.
* payload는 포함하지 않으며, trace 로그 설정이 켜진 경우에만 기록합니다.
*/ */
public void event(String event, Object... keyValues) { public void event(String event, Object... keyValues) {
if (properties.trace().enabled()) { 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) { public void error(String event, Throwable error, Object... keyValues) {
McpRequestContext context = McpRequestContextHolder.get().orElse(null); McpRequestContext context = McpRequestContextHolder.get().orElse(null);
@@ -59,7 +93,8 @@ public class TraceLogger {
} }
/** /**
* 가변 인자로 받은 키와 값을 두 개씩 묶어 읽기 쉬운 key=value 문자열로 바꿉니다. 홀수 개가 들어오면 짝이 없는 마지막 값은 기록하지 않습니다. * 가변 인자로 받은 key/value 쌍을 사람이 읽기 쉬운 key=value 문자열로 변환합니다.
* 홀수 개가 들어오면 짝이 없는 마지막 값은 기록하지 않습니다.
*/ */
private String fields(Object... keyValues) { private String fields(Object... keyValues) {
StringJoiner joiner = new StringJoiner(" "); 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) { private String safe(Object value) {
if (value == null) { if (value == null) {

View File

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.TextNode; import com.fasterxml.jackson.databind.node.TextNode;
import io.shinhanlife.dap.biz.mcp.config.McpProperties; import io.shinhanlife.dap.biz.mcp.config.McpProperties;
import io.shinhanlife.dap.biz.mcp.context.McpRequestContext; 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.ToolClientException;
import io.shinhanlife.dap.biz.mcp.toolclient.ToolClient.ToolRequest; import io.shinhanlife.dap.biz.mcp.toolclient.ToolClient.ToolRequest;
import io.shinhanlife.dap.biz.mcp.toolclient.ToolClient.ToolResponse; import io.shinhanlife.dap.biz.mcp.toolclient.ToolClient.ToolResponse;
@@ -30,6 +31,7 @@ public class HttpToolClient implements ToolClient {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final McpProperties properties; private final McpProperties properties;
private final HttpClient toolHttpClient; private final HttpClient toolHttpClient;
private final TraceLogger traceLogger;
/** /**
* JSON 변환, Tool 설정, 공유 connection pool을 가진 HTTP client를 주입받습니다. * JSON 변환, Tool 설정, 공유 connection pool을 가진 HTTP client를 주입받습니다.
@@ -37,10 +39,12 @@ public class HttpToolClient implements ToolClient {
public HttpToolClient( public HttpToolClient(
ObjectMapper objectMapper, ObjectMapper objectMapper,
McpProperties properties, McpProperties properties,
@Qualifier("toolHttpClient") HttpClient toolHttpClient) { @Qualifier("toolHttpClient") HttpClient toolHttpClient,
TraceLogger traceLogger) {
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.properties = properties; this.properties = properties;
this.toolHttpClient = toolHttpClient; this.toolHttpClient = toolHttpClient;
this.traceLogger = traceLogger;
} }
/** /**
@@ -49,6 +53,17 @@ public class HttpToolClient implements ToolClient {
@Override @Override
public ToolResponse execute(ToolRequest request, McpRequestContext context) { public ToolResponse execute(ToolRequest request, McpRequestContext context) {
try { 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); RestClient.RequestBodySpec spec = requestSpec(request, context);
var entity = var entity =
spec.retrieve() spec.retrieve()
@@ -58,9 +73,19 @@ public class HttpToolClient implements ToolClient {
throw statusException(response.getStatusCode().value(), request.toolName()); throw statusException(response.getStatusCode().value(), request.toolName());
}) })
.toEntity(String.class); .toEntity(String.class);
return new ToolResponse( JsonNode responseBody = parseResponse(entity.getBody(), entity.getHeaders().getContentType());
entity.getStatusCode().value(), traceLogger.payload(
parseResponse(entity.getBody(), entity.getHeaders().getContentType())); "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) { } catch (ToolClientException exception) {
throw exception; throw exception;
} catch (ResourceAccessException exception) { } catch (ResourceAccessException exception) {

View File

@@ -84,6 +84,15 @@ final class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
/** /**
* 요청의 문자 인코딩에 맞는 Reader를 반환합니다. 문자 인코딩이 없으면 JSON의 기본 인코딩인 UTF-8을 사용합니다. * 요청의 문자 인코딩에 맞는 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 @Override
public BufferedReader getReader() { public BufferedReader getReader() {
String encoding = getCharacterEncoding(); String encoding = getCharacterEncoding();

View File

@@ -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.JsonRpcRequestParser;
import io.shinhanlife.dap.biz.mcp.jsonrpc.JsonRpcResponse; import io.shinhanlife.dap.biz.mcp.jsonrpc.JsonRpcResponse;
import io.shinhanlife.dap.biz.mcp.method.McpMethodHandlerRegistry; import io.shinhanlife.dap.biz.mcp.method.McpMethodHandlerRegistry;
import io.shinhanlife.dap.biz.mcp.observability.TraceLogger;
import java.util.UUID; import java.util.UUID;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -27,14 +28,16 @@ public class McpController {
private final JsonRpcRequestParser requestParser; private final JsonRpcRequestParser requestParser;
private final McpMethodHandlerRegistry handlerRegistry; private final McpMethodHandlerRegistry handlerRegistry;
private final TraceLogger traceLogger;
/** /**
* JSON-RPC 변환과 method dispatch 협력 객체를 주입받습니다. Controller는 실행 규칙을 직접 구현하지 않고 각 책임 객체를 올바른 순서로 연결합니다. * JSON-RPC 변환과 method dispatch 협력 객체를 주입받습니다. Controller는 실행 규칙을 직접 구현하지 않고 각 책임 객체를 올바른 순서로 연결합니다.
*/ */
public McpController( public McpController(
JsonRpcRequestParser requestParser, McpMethodHandlerRegistry handlerRegistry) { JsonRpcRequestParser requestParser, McpMethodHandlerRegistry handlerRegistry, TraceLogger traceLogger) {
this.requestParser = requestParser; this.requestParser = requestParser;
this.handlerRegistry = handlerRegistry; this.handlerRegistry = handlerRegistry;
this.traceLogger = traceLogger;
} }
/** /**
@@ -52,6 +55,15 @@ public class McpController {
try { try {
JsonRpcResponse response = handlerRegistry.resolve(request.method()).handle(request, context); JsonRpcResponse response = handlerRegistry.resolve(request.method()).handle(request, context);
if (request.notification()) { if (request.notification()) {
traceLogger.payload(
"mcp_http_response_body",
null,
"direction",
"mcp_to_agent",
"mcpMethod",
request.method(),
"httpStatus",
202);
return ResponseEntity.accepted().build(); return ResponseEntity.accepted().build();
} }
ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.BodyBuilder responseBuilder =
@@ -59,6 +71,15 @@ public class McpController {
if ("initialize".equals(request.method())) { if ("initialize".equals(request.method())) {
responseBuilder.header(MCP_SESSION_ID_HEADER, UUID.randomUUID().toString()); 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); return responseBuilder.body(response);
} catch (JsonRpcException exception) { } catch (JsonRpcException exception) {
if (exception.requestId() != null) { if (exception.requestId() != null) {

View File

@@ -104,6 +104,17 @@ public class McpExchangeFilter extends OncePerRequestFilter {
request.getRequestURI(), request.getRequestURI(),
"mcpMethod", "mcpMethod",
mcpMethod); mcpMethod);
traceLogger.payload(
"mcp_http_request_body",
cachedRequest.bodyText(),
"direction",
"agent_to_mcp",
"httpMethod",
request.getMethod(),
"path",
request.getRequestURI(),
"mcpMethod",
mcpMethod);
try { try {
protocolVersionValidator.validatePostInitializeRequest(request, mcpMethod); protocolVersionValidator.validatePostInitializeRequest(request, mcpMethod);

View File

@@ -94,6 +94,9 @@ mcp:
bundles: [] bundles: []
trace: trace:
enabled: true 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. # Rejects oversized MCP request bodies before controller processing.
max-body-bytes: 1048576 max-body-bytes: 1048576

View File

@@ -29,7 +29,7 @@ public final class TestFixtures {
"file:./config/local-core-tools-manifest-sample-v1.json", 30, 5), "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.ToolClient(1_000, 5_000, 300_000, forwardAuthorization, "tool-server-key"),
new McpProperties.Redis(redisEnabled, "test:mcp:tools", "test:mcp:portal-registry"), 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.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.Discovery(!bundles.isEmpty(), 1_000, 3_000, 100, 200, 1_048_576, 30_000),
new McpProperties.Portal(false, "", "", 300), new McpProperties.Portal(false, "", "", 300),

View File

@@ -26,7 +26,7 @@ class TraceLoggerTest {
logger.addAppender(appender); logger.addAppender(appender);
McpRequestContextHolder.set(context()); 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); .event("mcp_http_response_completed", "httpStatus", 200);
assertThat(appender.list) assertThat(appender.list)

View File

@@ -41,7 +41,7 @@ class HttpToolClientTest {
.setHeader("Content-Type", "application/json") .setHeader("Content-Type", "application/json")
.setBody("{\"customerName\":\"홍길동\"}")); .setBody("{\"customerName\":\"홍길동\"}"));
HttpToolClient client = 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 = ToolRequest request =
new ToolRequest( new ToolRequest(
"customer.search", "customer.search",
@@ -75,7 +75,7 @@ class HttpToolClientTest {
server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") server.enqueue(new MockResponse().setHeader("Content-Type", "application/json")
.setBody("{\"quote\":\"시작이 반이다.\"}")); .setBody("{\"quote\":\"시작이 반이다.\"}"));
HttpToolClient client = 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( ToolRequest request = new ToolRequest(
"smp_quote_daily", "1.0.0", server.url("/mcp/smp_quote_daily").toString(), "smp_quote_daily", "1.0.0", server.url("/mcp/smp_quote_daily").toString(),
OBJECT_MAPPER.readTree("{\"category\":\"속담\"}"), 3_000); OBJECT_MAPPER.readTree("{\"category\":\"속담\"}"), 3_000);
@@ -97,7 +97,7 @@ class HttpToolClientTest {
void preservesPlainTextToolResponseAsTextNode() throws Exception { void preservesPlainTextToolResponseAsTextNode() throws Exception {
server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("123")); server.enqueue(new MockResponse().setHeader("Content-Type", "text/plain").setBody("123"));
HttpToolClient client = 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 = ToolRequest request =
new ToolRequest( new ToolRequest(
"processing", "processing",
@@ -116,7 +116,7 @@ class HttpToolClientTest {
void preservesHttpStatusOnToolError() throws Exception { void preservesHttpStatusOnToolError() throws Exception {
server.enqueue(new MockResponse().setResponseCode(410).setBody("gone")); server.enqueue(new MockResponse().setResponseCode(410).setBody("gone"));
HttpToolClient client = 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 = ToolRequest request =
new ToolRequest( new ToolRequest(
"customer.search", "customer.search",

View File

@@ -41,7 +41,7 @@ class McpControllerTest {
McpRequestContextHolder.set(context()); McpRequestContextHolder.set(context());
var response = 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.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
assertThat(response.getBody()).isNull(); assertThat(response.getBody()).isNull();
@@ -64,7 +64,7 @@ class McpControllerTest {
McpRequestContextHolder.set(context()); McpRequestContextHolder.set(context());
var response = 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); String sessionId = response.getHeaders().getFirst(McpController.MCP_SESSION_ID_HEADER);
assertThat(sessionId).isNotBlank(); assertThat(sessionId).isNotBlank();
@@ -89,7 +89,7 @@ class McpControllerTest {
McpRequestContextHolder.set(context()); McpRequestContextHolder.set(context());
var response = 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 = PostMapping mapping =
McpController.class McpController.class

View File

@@ -219,7 +219,7 @@ class McpExchangeFilterTest {
base.registry(), base.registry(),
base.toolClient(), base.toolClient(),
base.redis(), base.redis(),
new McpProperties.Trace(true, 8), new McpProperties.Trace(true, 8, false, 65_536),
base.protocol(), base.protocol(),
base.discovery(), base.discovery(),
base.portal(), base.portal(),
@@ -357,7 +357,7 @@ class McpExchangeFilterTest {
private McpExchangeFilter filter(McpProperties properties) { private McpExchangeFilter filter(McpProperties properties) {
return new McpExchangeFilter( return new McpExchangeFilter(
new McpRequestContextFactory(properties), new McpRequestContextFactory(properties),
new TraceLogger(properties), new TraceLogger(properties, io.shinhanlife.dap.biz.mcp.TestFixtures.OBJECT_MAPPER),
OBJECT_MAPPER, OBJECT_MAPPER,
properties, properties,
new McpProtocolVersionValidator(properties)); new McpProtocolVersionValidator(properties));