Files
ax_hub_mcp_tool/recent_changes.diff
jade 4b34df774d
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m15s
refactor: restructure packages, delete ZtUsac, fix gateway context
2026-08-05 10:09:36 +09:00

716 lines
34 KiB
Diff
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java
index c7251dd..6ee5f33 100644
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java
@@ -39,6 +39,17 @@ public @interface McpFunction {
*/
// 異붽?: 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 ???쇱슦?낆? ?섎굹 紐⑸줉?먯꽌 ?④?)
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java
new file mode 100644
index 0000000..cfb37dc
--- /dev/null
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java
@@ -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 {
+}
\ No newline at end of file
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java
index ecf23c8..4353edf 100644
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java
@@ -31,6 +31,6 @@ public @interface McpValidation {
int maxLength() default -1;
String[] allowedValues() default {};
String format() default "";
- String defaultValue() default "";
+ boolean nullable() default false; String defaultValue() default "";
String[] examples() default {};
}
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java
index a90efa0..d95dae6 100644
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java
@@ -103,6 +103,14 @@ public class JsonSchemaGenerator {
if (validation != null && validation.examples().length > 0) {
fieldSchema.put("examples", List.of(validation.examples()));
}
+ if (validation != null && validation.nullable()) {
+ Map<String, Object> nonNullSchema = new HashMap<>(fieldSchema);
+ fieldSchema = new HashMap<>();
+ fieldSchema.put("anyOf", List.of(
+ nonNullSchema,
+ Map.of("type", "null")
+ ));
+ }
properties.put(field.getName(), fieldSchema);
}
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java
index 787cbc1..5ea54df 100644
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java
@@ -3,7 +3,7 @@ package io.shinhanlife.dap.lib.util;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.annotation.McpFunction;
-import java.io.InputStream;
+import io.shinhanlife.dap.lib.annotation.McpOutputSchema;import java.io.InputStream;
import java.util.Map;
import org.springframework.core.io.ClassPathResource;
@@ -27,19 +27,44 @@ public class ToolSchemaResolver {
return JsonSchemaGenerator.generateSchema(requestType);
}
+ /**
+ * Resolves an explicitly declared response schema.
+ * Response schemas are opt-in so existing tools keep their current response behavior.
+ */
+ public Map<String, Object> resolveOutput(McpFunction function, Class<?> responseType) {
+ if (function != null && !function.outputSchemaResource().isBlank()) {
+ return loadResource(function.outputSchemaResource());
+ }
+ if (function != null && !function.outputSchema().isBlank()
+ && !"{}".equals(function.outputSchema().trim())) {
+ return parse(function.outputSchema(), "McpFunction.outputSchema");
+ }
+ if (responseType != null && responseType.isAnnotationPresent(McpOutputSchema.class)) {
+ return JsonSchemaGenerator.generateSchema(responseType);
+ }
+ return Map.of();
+ }
+
+ /**
+ * Retained for callers that use only explicit output schemas.
+ */
+ public Map<String, Object> resolveOutput(McpFunction function) {
+ return resolveOutput(function, null);
+ }
+
private Map<String, Object> loadResource(String location) {
String path = location.startsWith("classpath:")
? location.substring("classpath:".length())
: location;
ClassPathResource resource = new ClassPathResource(path);
if (!resource.exists()) {
- throw new IllegalStateException("MCP input schema resource not found: " + location);
+ throw new IllegalStateException("MCP schema resource not found: " + location);
}
try (InputStream inputStream = resource.getInputStream()) {
return objectMapper.readValue(inputStream, new TypeReference<>() { });
} catch (Exception e) {
- throw new IllegalStateException("Failed to load MCP input schema resource: " + location, e);
+ throw new IllegalStateException("Failed to load MCP schema resource: " + location, e);
}
}
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java
index daf0018..ec840ed 100644
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java
@@ -192,14 +192,30 @@ public class BusinessToolController {
} else {
methodResult = targetMethod.invoke(targetBean, invokeArgument);
}
+
+ Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(targetFunctionAnnotation, targetMethod.getReturnType());
+ if (!outputSchema.isEmpty()) {
+ List<Error> outputErrors = toolArgumentSchemaValidator.validateValue(outputSchema, methodResult);
+ if (!outputErrors.isEmpty()) {
+ log.error("[Tool] Output schema validation failed. tool={}, errors={}",
+ functionName, outputErrors);
+ Map<String, Object> errorBody = new HashMap<>();
+ errorBody.put("code", "INVALID_TOOL_RESPONSE");
+ errorBody.put("message", "Tool response does not match its output schema");
+ if (finalRequestId != null) {
+ errorBody.put("request_id", finalRequestId);
+ }
+ return ResponseEntity.internalServerError().body(errorBody);
+ }
+ }
long elapsed = System.currentTimeMillis() - startTime;
// 5. 寃곌낵 諛섑솚 (?쒖닔 REST ?묐떟)
try {
- log.info("[Tool -> MCP Gateway] ?숈쟻 ???ㅽ뻾 寃곌낵 諛섑솚: {}", objectMapper.writeValueAsString(methodResult));
+ log.info("[Tool -> MCP Gateway] Output Schema Result: {}", objectMapper.writeValueAsString(methodResult));
} catch (Exception e) {
- log.info("[Tool -> MCP Gateway] ?숈쟻 ???ㅽ뻾 寃곌낵 諛섑솚: {}", methodResult);
+ log.info("[Tool -> MCP Gateway] Output Schema Result: {}", methodResult);
}
log.info(" [Tool] OUT - trace-id: {}, request-id: {}", traceId, requestId);
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidator.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidator.java
index 876144c..59c68ff 100644
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidator.java
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidator.java
@@ -21,8 +21,12 @@ public class ToolArgumentSchemaValidator {
}
public List<Error> validate(Map<String, Object> schemaDefinition, Map<String, Object> arguments) throws Exception {
+ return validateValue(schemaDefinition, arguments);
+ }
+
+ public List<Error> validateValue(Map<String, Object> schemaDefinition, Object value) throws Exception {
SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7);
Schema schema = schemaRegistry.getSchema(objectMapper.writeValueAsString(schemaDefinition));
- return schema.validate(objectMapper.writeValueAsString(arguments), InputFormat.JSON);
+ return schema.validate(objectMapper.writeValueAsString(value), InputFormat.JSON);
}
}
diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java
index 6737a00..bc05f05 100644
--- a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java
+++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java
@@ -5,6 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.annotation.McpFunction;
+import io.shinhanlife.dap.lib.annotation.McpOutputSchema;
+import io.shinhanlife.dap.lib.annotation.McpValidation;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
@@ -15,66 +17,103 @@ class ToolSchemaResolverTest {
private final ToolSchemaResolver resolver = new ToolSchemaResolver(new ObjectMapper());
@Test
- void usesExplicitSchemaResourceBeforeAutomaticDtoSchema() throws Exception {
- Method method = SchemaBackedTool.class.getDeclaredMethod("search", AutoGeneratedRequest.class);
- McpFunction function = method.getAnnotation(McpFunction.class);
+ void usesInlineSchemaBeforeAutomaticDtoSchema() throws Exception {
+ Method method = InlineSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
- Map<String, Object> schema = resolver.resolve(function, AutoGeneratedRequest.class);
+ Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutomaticRequest.class);
assertEquals(false, schema.get("additionalProperties"));
- assertTrue(schema.containsKey("anyOf"));
- assertEquals(50, ((Map<?, ?>) ((Map<?, ?>) schema.get("properties")).get("size")).get("maximum"));
- assertEquals(List.of(Map.of("required", List.of("claimNo"))), schema.get("anyOf"));
+ assertTrue(!properties(schema).containsKey("differentField"));
}
+ @Test
+ void generatesSchemaFromRequestDtoWhenNoExplicitSchemaIsConfigured() throws Exception {
+ Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
- /* Inline schema resolution is covered by the same resolver branch at integration level.
- void usesInlineSchemaBeforeAutomaticDtoSchema() throws Exception {
- Method method = InlineSchemaTool.class.getDeclaredMethod("search", AutoGeneratedRequest.class);
+ Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutomaticRequest.class);
- Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutoGeneratedRequest.class);
+ assertTrue(properties(schema).containsKey("differentField"));
+ }
- assertEquals("object", schema.get("type"));
+ @Test
+ void resolvesExplicitOutputSchema() throws Exception {
+ Method method = OutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
+ Map<String, Object> schema = resolver.resolveOutput(method.getAnnotation(McpFunction.class));
assertEquals(false, schema.get("additionalProperties"));
- assertTrue(!((Map<?, ?>) schema.get("properties")).containsKey("differentField"));
+ assertTrue(properties(schema).containsKey("resultCode"));
}
- */
@Test
- void generatesSchemaFromRequestDtoWhenNoExplicitSchemaIsConfigured() throws Exception {
- Method method = SchemaBackedTool.AutomaticSchemaTool.class.getDeclaredMethod("search", AutoGeneratedRequest.class);
+ void generatesOutputSchemaFromMarkedResponseDto() throws Exception {
+ Method method = AutomaticOutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
- Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutoGeneratedRequest.class);
+ Map<String, Object> schema = resolver.resolveOutput(
+ method.getAnnotation(McpFunction.class), SimpleResponse.class);
- assertTrue(((Map<?, ?>) schema.get("properties")).containsKey("differentField"));
+ assertEquals(List.of("resultCode"), schema.get("required"));
+ assertEquals(List.of("SUCCESS", "FAILURE"), property(schema, "resultCode").get("enum"));
}
- static class InlineSchemaTool {
+ @Test
+ void doesNotEnableOutputValidationWhenOutputSchemaIsNotDeclared() throws Exception {
+ Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
+ Map<String, Object> schema = resolver.resolveOutput(
+ method.getAnnotation(McpFunction.class), AutomaticRequest.class);
+ assertTrue(schema.isEmpty());
+ }
+ @SuppressWarnings("unchecked")
+ private Map<String, Object> properties(Map<String, Object> schema) {
+ return (Map<String, Object>) schema.get("properties");
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map<String, Object> property(Map<String, Object> schema, String name) {
+ return (Map<String, Object>) properties(schema).get(name);
+ }
- @McpFunction(displayName = "inline", name = "sample.inline", description = "inline", inputSchema = "{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{},\\\"additionalProperties\\\":false}")
- void search(AutoGeneratedRequest request) {
+ static class InlineSchemaTool {
+ @McpFunction(
+ displayName = "inline",
+ name = "sample.inline",
+ description = "inline schema",
+ inputSchema = "{\"type\":\"object\",\"properties\":{\"keyword\":{\"type\":\"string\"}},\"additionalProperties\":false}")
+ void search(AutomaticRequest request) {
}
}
- static class SchemaBackedTool {
static class AutomaticSchemaTool {
+ @McpFunction(displayName = "automatic", name = "sample.automatic", description = "automatic schema")
+ void search(AutomaticRequest request) {
+ }
+ }
- @McpFunction(displayName = "automatic", name = "sample.automatic", description = "automatic")
- void search(AutoGeneratedRequest request) {
+ static class AutomaticOutputSchemaTool {
+ @McpFunction(displayName = "automatic-output", name = "sample.automatic-output", description = "automatic output")
+ SimpleResponse search(AutomaticRequest request) {
+ return null;
}
}
+ @McpOutputSchema
+ static class SimpleResponse {
+ @McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"})
+ private String resultCode;
+
+ @McpValidation(maxLength = 200)
+ private String message;
+ }
+ static class OutputSchemaTool {
@McpFunction(
- displayName = "泥?뎄 議고쉶",
- name = "processing.claim.search",
- description = "蹂댄뿕湲?泥?뎄瑜?議고쉶?쒕떎.",
- inputSchemaResource = "classpath:tool-schemas/claim-search-input-schema.json")
- void search(AutoGeneratedRequest request) {
+ displayName = "output",
+ name = "sample.output",
+ description = "output schema",
+ outputSchema = "{\"type\":\"object\",\"properties\":{\"resultCode\":{\"type\":\"string\"}},\"required\":[\"resultCode\"],\"additionalProperties\":false}")
+ void search(AutomaticRequest request) {
}
}
- static class AutoGeneratedRequest {
+ static class AutomaticRequest {
private String differentField;
}
}
diff --git a/dap-tool-core/src/test/resources/tool-schemas/claim-search-input-schema.json b/dap-tool-core/src/test/resources/tool-schemas/claim-search-input-schema.json
deleted file mode 100644
index e595b42..0000000
--- a/dap-tool-core/src/test/resources/tool-schemas/claim-search-input-schema.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
- "properties": {
- "claimNo": {
- "type": "string",
- "pattern": "^CLM[0-9]{13}$"
- },
- "size": {
- "type": "integer",R
- "minimum": 1,
- "maximum": 50,
- "default": 20
- }
- },
- "additionalProperties": false,
- "anyOf": [
- { "required": ["claimNo"] }
- ]
-}
diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java
new file mode 100644
index 0000000..abd2d2d
--- /dev/null
+++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java
@@ -0,0 +1,88 @@
+package io.shinhanlife.dap.mcc.biz.cmm.dto;
+
+import io.shinhanlife.dap.lib.annotation.McpOutputSchema;
+import io.shinhanlife.dap.lib.annotation.McpParameter;
+import io.shinhanlife.dap.lib.annotation.McpValidation;
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * Claim search response sample.
+ * Complex response rules are defined by outputSchemaResource; annotations document
+ * the same simple field constraints for automatic schema generation examples.
+ */
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@McpOutputSchema
+public class ClaimSearchResponse {
+
+ @McpParameter(description = "Execution result code.")
+ @McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"})
+ private String resultCode;
+
+ @McpParameter(description = "User-readable label for resultCode.")
+ @McpValidation(required = true, maxLength = 100)
+ private String resultLabel;
+
+ @McpParameter(description = "Current claim processing status code.")
+ @McpValidation(required = true, allowedValues = {
+ "RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED",
+ "APPROVED", "PAID", "REJECTED", "WITHDRAWN"
+ })
+ private String status;
+
+ @McpParameter(description = "User-readable label for status.")
+ @McpValidation(required = true, maxLength = 100)
+ private String statusLabel;
+
+ @McpParameter(description = "Approved amount. Null before review; do not interpret null as zero.")
+ @McpValidation(minimum = 0, nullable = true)
+ private Long approvedAmount;
+
+ @McpParameter(description = "Present only when status is REJECTED; otherwise null.")
+ @McpValidation(maxLength = 200, nullable = true)
+ private String rejectionReason;
+
+ @McpParameter(description = "Claim summaries, ordered by received date descending.")
+ @McpValidation(required = true)
+ private List<ClaimSummary> items;
+
+ @McpParameter(description = "True when additional results exist beyond this response.")
+ @McpValidation(required = true)
+ private Boolean hasMore;
+
+ @McpParameter(description = "Total number of matched claims.")
+ @McpValidation(required = true, minimum = 0)
+ private Integer totalCount;
+
+ @Getter
+ @Setter
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class ClaimSummary {
+
+ @McpParameter(description = "Claim processing status code.")
+ @McpValidation(required = true)
+ private String status;
+
+ @McpParameter(description = "User-readable label for status.")
+ @McpValidation(required = true)
+ private String statusLabel;
+
+ @McpParameter(description = "Received date in YYYY-MM-DD format.")
+ @McpValidation(required = true, format = "date")
+ private String receivedDate;
+
+ @McpParameter(description = "Approved amount. Null before review; do not interpret null as zero.")
+ @McpValidation(minimum = 0, nullable = true)
+ private Long approvedAmount;
+ }
+}
\ No newline at end of file
diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java
index 16d1aa0..9a55c1a 100644
--- a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java
+++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java
@@ -3,6 +3,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.usecase;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest;
+import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
@McpTool(
routingType = "DIRECT",
@@ -12,13 +13,14 @@ public interface ClaimSearchSchemaSampleUseCase {
@McpFunction(
register = false,
- displayName = "泥?뎄 議고쉶 JSON Schema ?섑뵆",
+ displayName = "Claim search JSON Schema sample",
name = "sample.claim.search.resource",
- description = "inputSchemaResource瑜??ъ슜?섎뒗 泥?뎄 議고쉶 Tool ?섑뵆?낅땲??",
- prompt = "泥?뎄踰덊샇 ?먮뒗 怨꾩빟踰덊샇濡?蹂댄뿕湲?泥?뎄瑜?議고쉶?댁쨾.",
- inputSchemaResource = "classpath:tool-schemas/claim-search-resource-input-schema.json",
+ description = "Claim search Tool sample using input and output JSON Schema resources.",
+ prompt = "Search an insurance claim by claim number or contract number.",
+ inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json",
+ outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json",
readOnlyHint = true,
idempotentHint = true
)
- Object search(ClaimSearchRequest request);
-}
+ ClaimSearchResponse search(ClaimSearchRequest request);
+}
\ No newline at end of file
diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java
index e6cd98b..1a817f5 100644
--- a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java
+++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java
@@ -1,22 +1,36 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest;
+import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
import io.shinhanlife.dap.mcc.biz.cmm.usecase.ClaimSearchSchemaSampleUseCase;
-import java.util.Map;
+import java.util.List;
import org.springframework.stereotype.Service;
/**
- * inputSchemaResource ?곸슜 諛⑹떇??蹂댁뿬二쇰뒗 鍮꾨끂異??섑뵆 Tool?대떎.
- * ?ㅼ젣 MCI/EIMS ?곕룞?€ 異붽??섏? ?딅뒗??
+ * Non-exposed sample Tool. It does not call MCI or EIMS.
*/
@Service
public class ClaimSearchSchemaSampleUseCaseImpl implements ClaimSearchSchemaSampleUseCase {
@Override
- public Object search(ClaimSearchRequest request) {
- return Map.of(
- "message", "inputSchemaResource JSON Schema sample",
- "request", request == null ? Map.of() : request
- );
+ public ClaimSearchResponse search(ClaimSearchRequest request) {
+ ClaimSearchResponse.ClaimSummary item = ClaimSearchResponse.ClaimSummary.builder()
+ .status("REVIEWING")
+ .statusLabel("Under review")
+ .receivedDate("2026-08-04")
+ .approvedAmount(null)
+ .build();
+
+ return ClaimSearchResponse.builder()
+ .resultCode("SUCCESS")
+ .resultLabel("Success")
+ .status("REVIEWING")
+ .statusLabel("Under review")
+ .approvedAmount(null)
+ .rejectionReason(null)
+ .items(List.of(item))
+ .hasMore(false)
+ .totalCount(1)
+ .build();
}
-}
+}
\ No newline at end of file
diff --git a/dap-tool-oth/src/main/resources/tool-schemas/claim-search-resource-input-schema.json b/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-input-schema.json
similarity index 100%
rename from dap-tool-oth/src/main/resources/tool-schemas/claim-search-resource-input-schema.json
rename to dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-input-schema.json
diff --git a/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json b/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json
new file mode 100644
index 0000000..f94c661
--- /dev/null
+++ b/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json
@@ -0,0 +1,81 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "description": "Claim search response. This schema intentionally excludes employee identifiers, customer names, contact details, account information, and other PII.",
+ "properties": {
+ "resultCode": {
+ "type": "string",
+ "enum": ["SUCCESS", "FAILURE"],
+ "description": "Machine-readable execution result code."
+ },
+ "resultLabel": {
+ "type": "string",
+ "description": "User-readable label for resultCode."
+ },
+ "status": {
+ "type": "string",
+ "enum": ["RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED", "APPROVED", "PAID", "REJECTED", "WITHDRAWN"],
+ "description": "Current claim processing status code."
+ },
+ "statusLabel": {
+ "type": "string",
+ "description": "User-readable label for status."
+ },
+ "approvedAmount": {
+ "anyOf": [
+ { "type": "number", "minimum": 0 },
+ { "type": "null" }
+ ],
+ "description": "Approved amount. It is null before review and must not be interpreted as zero."
+ },
+ "rejectionReason": {
+ "type": ["string", "null"],
+ "maxLength": 200,
+ "description": "Has a value only when status is REJECTED. It is null for every other status."
+ },
+ "items": {
+ "type": "array",
+ "description": "Claim summaries ordered by receivedDate descending. No personally identifiable information is included.",
+ "items": {
+ "type": "object",
+ "properties": {
+ "status": { "type": "string", "description": "Claim status code." },
+ "statusLabel": { "type": "string", "description": "User-readable label for status." },
+ "receivedDate": { "type": "string", "format": "date", "description": "Claim received date." },
+ "approvedAmount": {
+ "anyOf": [
+ { "type": "number", "minimum": 0 },
+ { "type": "null" }
+ ],
+ "description": "Null before review; do not interpret as zero."
+ }
+ },
+ "required": ["status", "statusLabel", "receivedDate"],
+ "additionalProperties": false
+ }
+ },
+ "hasMore": {
+ "type": "boolean",
+ "description": "True when additional results exist beyond this response."
+ },
+ "totalCount": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Total number of matched claims."
+ }
+ },
+ "required": ["resultCode", "resultLabel", "status", "statusLabel", "items", "hasMore", "totalCount"],
+ "allOf": [
+ {
+ "if": {
+ "properties": { "status": { "const": "REJECTED" } },
+ "required": ["status"]
+ },
+ "then": {
+ "properties": { "rejectionReason": { "type": "string", "minLength": 1 } },
+ "required": ["rejectionReason"]
+ }
+ }
+ ],
+ "additionalProperties": false
+}
\ No newline at end of file
diff --git a/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java
index b52e10c..cead062 100644
--- a/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java
+++ b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java
@@ -3,7 +3,13 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.util.JsonSchemaGenerator;
+import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
+import io.shinhanlife.dap.mcc.biz.cmm.usecase.impl.ClaimSearchSchemaSampleUseCaseImpl;
+import io.shinhanlife.dap.mcc.presentation.ToolArgumentSchemaValidator;import io.shinhanlife.dap.mcc.biz.cmm.usecase.ClaimSearchSchemaSampleUseCase;
+import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
@@ -24,8 +30,59 @@ class ClaimSearchRequestSchemaTest {
Map.of("required", List.of("contractNo"))), schema.get("anyOf"));
}
+ @Test
+ void resolvesSchemaFromToolModuleResource() throws Exception {
+ Method method = ClaimSearchSchemaSampleUseCase.class
+ .getDeclaredMethod("search", ClaimSearchRequest.class);
+ McpFunction function = method.getAnnotation(McpFunction.class);
+
+ Map<String, Object> schema = new ToolSchemaResolver(new ObjectMapper())
+ .resolve(function, ClaimSearchRequest.class);
+
+ assertEquals("https://json-schema.org/draft/2020-12/schema", schema.get("$schema"));
+ assertTrue(schema.containsKey("anyOf"));
+ assertEquals(50, property(schema, "size").get("maximum"));
+ }
+
+ @Test
+ void resolvesOutputSchemaFromToolModuleResource() throws Exception {
+ Method method = ClaimSearchSchemaSampleUseCase.class
+ .getDeclaredMethod("search", ClaimSearchRequest.class);
+ McpFunction function = method.getAnnotation(McpFunction.class);
+ Map<String, Object> schema = new ToolSchemaResolver(new ObjectMapper()).resolveOutput(function);
+ assertEquals("https://json-schema.org/draft/2020-12/schema", schema.get("$schema"));
+ assertTrue(properties(schema).containsKey("resultCode"));
+ assertTrue(properties(schema).containsKey("statusLabel"));
+ assertTrue(properties(schema).containsKey("hasMore"));
+ assertTrue(schema.containsKey("allOf"));
+ }
+
+ @Test
+ void sampleResponseConformsToOutputSchema() throws Exception {
+ Method method = ClaimSearchSchemaSampleUseCase.class
+ .getDeclaredMethod("search", ClaimSearchRequest.class);
+ Map<String, Object> schema = new ToolSchemaResolver(new ObjectMapper())
+ .resolveOutput(method.getAnnotation(McpFunction.class));
+
+ ClaimSearchResponse response = new ClaimSearchSchemaSampleUseCaseImpl().search(new ClaimSearchRequest());
+ ToolArgumentSchemaValidator validator = new ToolArgumentSchemaValidator(new ObjectMapper());
+
+ assertTrue(validator.validateValue(schema, response).isEmpty());
+ }
+ @Test
+ void sampleResponseConformsToAutomaticallyGeneratedOutputSchema() throws Exception {
+ Map<String, Object> schema = JsonSchemaGenerator.generateSchema(ClaimSearchResponse.class);
+ ClaimSearchResponse response = new ClaimSearchSchemaSampleUseCaseImpl().search(new ClaimSearchRequest());
+ ToolArgumentSchemaValidator validator = new ToolArgumentSchemaValidator(new ObjectMapper());
+
+ assertTrue(validator.validateValue(schema, response).isEmpty());
+ }
@SuppressWarnings("unchecked")
private Map<String, Map<String, Object>> properties(Map<String, Object> schema) {
return (Map<String, Map<String, Object>>) schema.get("properties");
}
+
+ private Map<String, Object> property(Map<String, Object> schema, String name) {
+ return properties(schema).get(name);
+ }
}