From beebc6d7c3ff2ca7ff3f0c429c3f0296054bf499 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:20:45 +0300 Subject: [PATCH 1/5] feat: implemented MCP trigger --- pom.xml | 35 +++++-- .../engine/camel/JsonMessageValidator.java | 50 +++------- .../engine/model/ChainElementType.java | 1 + .../StringToTimeValueConverter.java | 2 +- .../service/debugger/logging/ChainLogger.java | 2 +- .../context/create/McpToolRegistrar.java | 99 +++++++++++++++++++ .../context/stop/McpToolUnregisterAction.java | 44 +++++++++ src/main/resources/application.yml | 16 +++ 8 files changed, 202 insertions(+), 47 deletions(-) create mode 100644 src/main/java/org/qubership/integration/platform/engine/service/deployment/processing/actions/context/create/McpToolRegistrar.java create mode 100644 src/main/java/org/qubership/integration/platform/engine/service/deployment/processing/actions/context/stop/McpToolUnregisterAction.java diff --git a/pom.xml b/pom.xml index e7208714..f59e023d 100644 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,7 @@ 4.14.5 2025.0.1 + 1.1.3 1.1.10.8 3.7.4 2.5.2 @@ -57,7 +58,8 @@ 1.5.5.Final 0.2.0 1.18.42 - 1.0.87 + + 19.0.3 3.25.8 5.20.0 @@ -121,6 +123,14 @@ import + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + @@ -203,11 +213,11 @@ ${atlasmap.version} - - com.networknt - json-schema-validator - ${json-schema-validator.version} - + + + + + io.kubernetes @@ -577,10 +587,10 @@ org.springframework spring-webflux - - com.networknt - json-schema-validator - + + + + io.kubernetes @@ -656,6 +666,11 @@ at.yawk.lz4 lz4-java + + + org.springframework.ai + spring-ai-starter-mcp-server-webmvc + diff --git a/src/main/java/org/qubership/integration/platform/engine/camel/JsonMessageValidator.java b/src/main/java/org/qubership/integration/platform/engine/camel/JsonMessageValidator.java index 139d01eb..074f5ae7 100644 --- a/src/main/java/org/qubership/integration/platform/engine/camel/JsonMessageValidator.java +++ b/src/main/java/org/qubership/integration/platform/engine/camel/JsonMessageValidator.java @@ -16,55 +16,35 @@ package org.qubership.integration.platform.engine.camel; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.JsonSchema; -import com.networknt.schema.JsonSchemaFactory; -import com.networknt.schema.SpecVersion; -import com.networknt.schema.ValidationMessage; +import com.networknt.schema.*; +import com.networknt.schema.Error; import org.apache.commons.lang3.StringUtils; import org.qubership.integration.platform.engine.errorhandling.ValidationException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; -import java.util.Set; +import java.util.List; import java.util.stream.Collectors; @Component public class JsonMessageValidator { public static final String MESSAGE_VALIDATION_ERROR = "Errors during message validation: "; - private static final String PARSE_MESSAGE_BODY_ERROR = "Unable to parse message body"; private static final String EMPTY_BODY_ERROR = "Message body is empty"; - private final ObjectMapper objectMapper; - - @Autowired - public JsonMessageValidator(@Qualifier("jsonMapper") ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - } - public void validate(String jsonMessageAsString, String jsonSchemaAsString) { - try { - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); - JsonSchema schemaNode = factory.getSchema(jsonSchemaAsString); + SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7); + Schema schema = schemaRegistry.getSchema(jsonSchemaAsString); - if (StringUtils.isBlank(jsonMessageAsString)) { - throw new ValidationException(EMPTY_BODY_ERROR); - } + if (StringUtils.isBlank(jsonMessageAsString)) { + throw new ValidationException(EMPTY_BODY_ERROR); + } - JsonNode messageNode = objectMapper.readTree(jsonMessageAsString); - Set errors = schemaNode.validate(messageNode); - if (!errors.isEmpty()) { - String validationMessages = errors - .stream() - .map(ValidationMessage::getMessage) - .collect(Collectors.joining(", ")); - throw new ValidationException(MESSAGE_VALIDATION_ERROR.concat(validationMessages)); - } - } catch (JsonProcessingException e) { - throw new ValidationException(PARSE_MESSAGE_BODY_ERROR); + List errors = schema.validate(jsonMessageAsString, InputFormat.JSON); + if (!errors.isEmpty()) { + String validationMessages = errors + .stream() + .map(Error::getMessage) + .collect(Collectors.joining(", ")); + throw new ValidationException(MESSAGE_VALIDATION_ERROR.concat(validationMessages)); } } } diff --git a/src/main/java/org/qubership/integration/platform/engine/model/ChainElementType.java b/src/main/java/org/qubership/integration/platform/engine/model/ChainElementType.java index 50fdf453..aff443e4 100644 --- a/src/main/java/org/qubership/integration/platform/engine/model/ChainElementType.java +++ b/src/main/java/org/qubership/integration/platform/engine/model/ChainElementType.java @@ -67,6 +67,7 @@ public enum ChainElementType { SPLIT_ASYNC_2("split-async-2"), ASYNC_SPLIT_ELEMENT_2("async-split-element-2"), FINALLY_2("finally-2"), + MCP_TRIGGER("mcp-trigger"), UNKNOWN(""); // add more elements as needed diff --git a/src/main/java/org/qubership/integration/platform/engine/opensearch/ism/converters/StringToTimeValueConverter.java b/src/main/java/org/qubership/integration/platform/engine/opensearch/ism/converters/StringToTimeValueConverter.java index 5f79a5a6..458ffd6d 100644 --- a/src/main/java/org/qubership/integration/platform/engine/opensearch/ism/converters/StringToTimeValueConverter.java +++ b/src/main/java/org/qubership/integration/platform/engine/opensearch/ism/converters/StringToTimeValueConverter.java @@ -16,7 +16,7 @@ package org.qubership.integration.platform.engine.opensearch.ism.converters; -import com.networknt.schema.utils.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.qubership.integration.platform.engine.opensearch.ism.model.time.TimeValue; import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; import org.springframework.core.convert.converter.Converter; diff --git a/src/main/java/org/qubership/integration/platform/engine/service/debugger/logging/ChainLogger.java b/src/main/java/org/qubership/integration/platform/engine/service/debugger/logging/ChainLogger.java index d3a3d146..22390623 100644 --- a/src/main/java/org/qubership/integration/platform/engine/service/debugger/logging/ChainLogger.java +++ b/src/main/java/org/qubership/integration/platform/engine/service/debugger/logging/ChainLogger.java @@ -16,7 +16,6 @@ package org.qubership.integration.platform.engine.service.debugger.logging; -import com.networknt.schema.utils.StringUtils; import lombok.extern.slf4j.Slf4j; import org.apache.camel.CamelException; import org.apache.camel.Exchange; @@ -25,6 +24,7 @@ import org.apache.camel.support.http.HttpUtil; import org.apache.camel.tracing.ActiveSpanManager; import org.apache.camel.tracing.SpanAdapter; +import org.apache.commons.lang3.StringUtils; import org.qubership.integration.platform.engine.errorhandling.errorcode.ErrorCode; import org.qubership.integration.platform.engine.model.ChainElementType; import org.qubership.integration.platform.engine.model.constants.CamelConstants; diff --git a/src/main/java/org/qubership/integration/platform/engine/service/deployment/processing/actions/context/create/McpToolRegistrar.java b/src/main/java/org/qubership/integration/platform/engine/service/deployment/processing/actions/context/create/McpToolRegistrar.java new file mode 100644 index 00000000..331e4128 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/engine/service/deployment/processing/actions/context/create/McpToolRegistrar.java @@ -0,0 +1,99 @@ +package org.qubership.integration.platform.engine.service.deployment.processing.actions.context.create; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; +import io.modelcontextprotocol.server.McpServerFeatures; +import io.modelcontextprotocol.server.McpSyncServer; +import io.modelcontextprotocol.spec.McpSchema; +import lombok.extern.slf4j.Slf4j; +import org.apache.camel.Exchange; +import org.apache.camel.ProducerTemplate; +import org.apache.camel.spring.SpringCamelContext; +import org.apache.commons.lang3.StringUtils; +import org.qubership.integration.platform.engine.model.ChainElementType; +import org.qubership.integration.platform.engine.model.constants.CamelConstants; +import org.qubership.integration.platform.engine.model.deployment.update.DeploymentInfo; +import org.qubership.integration.platform.engine.model.deployment.update.ElementProperties; +import org.qubership.integration.platform.engine.service.deployment.processing.ElementProcessingAction; +import org.qubership.integration.platform.engine.service.deployment.processing.qualifiers.OnAfterDeploymentContextCreated; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.Map; + +import static java.util.Objects.isNull; + +@Slf4j +@Component +@OnAfterDeploymentContextCreated +public class McpToolRegistrar extends ElementProcessingAction { + public static final String DEPLOYMENT_ID = "deploymentId"; + + private final McpSyncServer mcpSyncServer; + private final McpJsonMapper mcpJsonMapper; + + @Autowired + public McpToolRegistrar( + McpSyncServer mcpSyncServer, + @Qualifier("jsonMapper") ObjectMapper objectMapper + ) { + this.mcpSyncServer = mcpSyncServer; + this.mcpJsonMapper = new JacksonMcpJsonMapper(objectMapper); + } + + @Override + public boolean applicableTo(ElementProperties properties) { + String elementType = properties.getProperties().get(CamelConstants.ChainProperties.ELEMENT_TYPE); + ChainElementType chainElementType = ChainElementType.fromString(elementType); + return ChainElementType.MCP_TRIGGER.equals(chainElementType); + } + + @Override + public void apply(SpringCamelContext context, ElementProperties properties, DeploymentInfo deploymentInfo) { + McpSchema.Tool tool = buildMcpTool(properties, deploymentInfo); + McpServerFeatures.SyncToolSpecification toolSpecification = McpServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler((mcpExchange, request) -> { + ProducerTemplate producerTemplate = context.createProducerTemplate(); + String endpointUri = "direct:" + properties.getElementId(); + Exchange result = producerTemplate.request(endpointUri, exchange -> + exchange.getIn().setBody(request.arguments())); + McpSchema.CallToolResult.Builder builder = McpSchema.CallToolResult.builder(); + if (isNull(tool.outputSchema())) { + builder.textContent(Collections.singletonList(result.getMessage().getBody(String.class))); + } else { + builder.structuredContent(result.getMessage().getBody()); + } + return builder.build(); + }) + .build(); + log.debug("Registering MCP tool: {}", toolSpecification.tool()); + mcpSyncServer.addTool(toolSpecification); + } + + private McpSchema.Tool buildMcpTool(ElementProperties properties, DeploymentInfo deploymentInfo) { + Map props = properties.getProperties(); + McpSchema.Tool.Builder toolBuilder = McpSchema.Tool.builder(); + toolBuilder + .name(props.get("name")) + .description(props.get("description")) + .title(props.get("title")) + .annotations(new McpSchema.ToolAnnotations( + props.get("title"), + Boolean.valueOf(props.get("readOnly")), + Boolean.valueOf(props.get("destructive")), + Boolean.valueOf(props.get("idempotent")), + Boolean.valueOf(props.get("openWorld")), + Boolean.valueOf(props.get("requiresLocal")))) + .meta(Map.of(DEPLOYMENT_ID, deploymentInfo.getDeploymentId())) + .inputSchema(mcpJsonMapper, props.get("inputSchema")); + String outputSchema = props.get("outputSchema"); + if (StringUtils.isNotBlank(outputSchema)) { + toolBuilder.outputSchema(mcpJsonMapper, outputSchema); + } + return toolBuilder.build(); + } +} diff --git a/src/main/java/org/qubership/integration/platform/engine/service/deployment/processing/actions/context/stop/McpToolUnregisterAction.java b/src/main/java/org/qubership/integration/platform/engine/service/deployment/processing/actions/context/stop/McpToolUnregisterAction.java new file mode 100644 index 00000000..4821dee8 --- /dev/null +++ b/src/main/java/org/qubership/integration/platform/engine/service/deployment/processing/actions/context/stop/McpToolUnregisterAction.java @@ -0,0 +1,44 @@ +package org.qubership.integration.platform.engine.service.deployment.processing.actions.context.stop; + +import io.modelcontextprotocol.server.McpSyncServer; +import io.modelcontextprotocol.spec.McpSchema; +import lombok.extern.slf4j.Slf4j; +import org.apache.camel.spring.SpringCamelContext; +import org.qubership.integration.platform.engine.model.deployment.update.DeploymentConfiguration; +import org.qubership.integration.platform.engine.model.deployment.update.DeploymentInfo; +import org.qubership.integration.platform.engine.service.deployment.processing.DeploymentProcessingAction; +import org.qubership.integration.platform.engine.service.deployment.processing.qualifiers.OnStopDeploymentContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +import static org.qubership.integration.platform.engine.service.deployment.processing.actions.context.create.McpToolRegistrar.DEPLOYMENT_ID; + +@Slf4j +@Component +@OnStopDeploymentContext +public class McpToolUnregisterAction implements DeploymentProcessingAction { + private final McpSyncServer mcpSyncServer; + + @Autowired + public McpToolUnregisterAction( + McpSyncServer mcpSyncServer + ) { + this.mcpSyncServer = mcpSyncServer; + } + + @Override + public void execute( + SpringCamelContext context, + DeploymentInfo deploymentInfo, + DeploymentConfiguration deploymentConfiguration + ) { + List toolsToRemove = mcpSyncServer.listTools() + .stream() + .filter(tool -> deploymentInfo.getDeploymentId() + .equals(tool.meta().get(DEPLOYMENT_ID))) + .toList(); + toolsToRemove.forEach(tool -> mcpSyncServer.removeTool(tool.name())); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8ef96f8b..c86f1015 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -67,6 +67,22 @@ spring: serialization: indent-output: true + ai: + mcp: + server: + enabled: true + type: SYNC + protocol: STREAMABLE + annotation-scanner: + enabled: true + streamable-http: + mcp-endpoint: /mcp + capabilities: + completion: false + tool: true + resource: false + prompt: false + app: prefix: qip From facedb09a3a4452c2502e126838f208ba4e614a7 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:29:49 +0300 Subject: [PATCH 2/5] feat: added unit tests for McpToolRegistrar and McpToolUnregisterAction classes --- pom.xml | 6 + .../context/create/McpToolRegistrarTest.java | 186 ++++++++++++++++++ .../stop/McpToolUnregisterActionTest.java | 94 +++++++++ 3 files changed, 286 insertions(+) create mode 100644 src/test/java/org/qubership/integration/platform/engine/service/deployment/processing/actions/context/create/McpToolRegistrarTest.java create mode 100644 src/test/java/org/qubership/integration/platform/engine/service/deployment/processing/actions/context/stop/McpToolUnregisterActionTest.java diff --git a/pom.xml b/pom.xml index f59e023d..3375e7f6 100644 --- a/pom.xml +++ b/pom.xml @@ -391,6 +391,12 @@ test + + org.mockito + mockito-core + test + + de.siegmar logback-gelf diff --git a/src/test/java/org/qubership/integration/platform/engine/service/deployment/processing/actions/context/create/McpToolRegistrarTest.java b/src/test/java/org/qubership/integration/platform/engine/service/deployment/processing/actions/context/create/McpToolRegistrarTest.java new file mode 100644 index 00000000..c282c38f --- /dev/null +++ b/src/test/java/org/qubership/integration/platform/engine/service/deployment/processing/actions/context/create/McpToolRegistrarTest.java @@ -0,0 +1,186 @@ +package org.qubership.integration.platform.engine.service.deployment.processing.actions.context.create; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.server.McpServerFeatures; +import io.modelcontextprotocol.server.McpSyncServer; +import io.modelcontextprotocol.spec.McpSchema; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.qubership.integration.platform.engine.model.constants.CamelConstants.ChainProperties; +import org.qubership.integration.platform.engine.model.deployment.update.DeploymentConfiguration; +import org.qubership.integration.platform.engine.model.deployment.update.DeploymentInfo; +import org.qubership.integration.platform.engine.model.deployment.update.ElementProperties; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class McpToolRegistrarTest { + + private static final String DEPLOYMENT_ID = "deployment-123"; + private static final String ELEMENT_ID = "element-456"; + private static final String TOOL_NAME = "my-tool"; + private static final String TOOL_DESCRIPTION = "A test tool"; + private static final String TOOL_TITLE = "My Tool"; + private static final String INPUT_SCHEMA = "{\"type\":\"object\",\"properties\":{\"param\":{\"type\":\"string\"}}}"; + private static final String OUTPUT_SCHEMA = "{\"type\":\"object\",\"properties\":{\"result\":{\"type\":\"string\"}}}"; + + private McpSyncServer mcpSyncServer; + private McpToolRegistrar registrar; + + @BeforeEach + void setUp() { + mcpSyncServer = mock(McpSyncServer.class); + registrar = new McpToolRegistrar(mcpSyncServer, new ObjectMapper()); + } + + @Test + void applicableToReturnsTrueForMcpTrigger() { + ElementProperties properties = elementProperties("mcp-trigger"); + assertTrue(registrar.applicableTo(properties)); + } + + @Test + void applicableToReturnsFalseForHttpTrigger() { + ElementProperties properties = elementProperties("http-trigger"); + assertFalse(registrar.applicableTo(properties)); + } + + @Test + void applicableToReturnsFalseForUnknownType() { + ElementProperties properties = elementProperties("unknown-type"); + assertFalse(registrar.applicableTo(properties)); + } + + @Test + void applicableToReturnsFalseWhenElementTypeIsNull() { + ElementProperties properties = ElementProperties.builder() + .elementId(ELEMENT_ID) + .properties(new HashMap<>()) + .build(); + assertFalse(registrar.applicableTo(properties)); + } + + @Test + void applyRegistersToolWithMcpServer() { + ElementProperties properties = mcpTriggerProperties(null); + DeploymentInfo deploymentInfo = deploymentInfo(); + + registrar.apply(null, properties, deploymentInfo); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(McpServerFeatures.SyncToolSpecification.class); + verify(mcpSyncServer).addTool(captor.capture()); + + McpSchema.Tool tool = captor.getValue().tool(); + assertEquals(TOOL_NAME, tool.name()); + assertEquals(TOOL_DESCRIPTION, tool.description()); + assertEquals(TOOL_TITLE, tool.title()); + assertEquals(DEPLOYMENT_ID, tool.meta().get(McpToolRegistrar.DEPLOYMENT_ID)); + } + + @Test + void applyRegistersToolWithOutputSchemaWhenProvided() { + ElementProperties properties = mcpTriggerProperties(OUTPUT_SCHEMA); + DeploymentInfo deploymentInfo = deploymentInfo(); + + registrar.apply(null, properties, deploymentInfo); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(McpServerFeatures.SyncToolSpecification.class); + verify(mcpSyncServer).addTool(captor.capture()); + + McpSchema.Tool tool = captor.getValue().tool(); + assertNotNull(tool.outputSchema()); + } + + @Test + void applyRegistersToolWithoutOutputSchemaWhenBlank() { + ElementProperties properties = mcpTriggerProperties(""); + DeploymentInfo deploymentInfo = deploymentInfo(); + + registrar.apply(null, properties, deploymentInfo); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(McpServerFeatures.SyncToolSpecification.class); + verify(mcpSyncServer).addTool(captor.capture()); + + McpSchema.Tool tool = captor.getValue().tool(); + assertNull(tool.outputSchema()); + } + + @Test + void applyRegistersToolWithCorrectAnnotations() { + ElementProperties properties = mcpTriggerProperties(null); + DeploymentInfo deploymentInfo = deploymentInfo(); + + registrar.apply(null, properties, deploymentInfo); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(McpServerFeatures.SyncToolSpecification.class); + verify(mcpSyncServer).addTool(captor.capture()); + + McpSchema.ToolAnnotations annotations = captor.getValue().tool().annotations(); + assertNotNull(annotations); + assertTrue(annotations.readOnlyHint()); + assertFalse(annotations.destructiveHint()); + } + + @Test + void executeAppliesOnlyMcpTriggerElements() { + ElementProperties mcpElement = mcpTriggerProperties(null); + ElementProperties httpElement = elementProperties("http-trigger"); + httpElement.getProperties().put("name", "http-tool"); + DeploymentConfiguration config = DeploymentConfiguration.builder() + .properties(List.of(mcpElement, httpElement)) + .build(); + DeploymentInfo deploymentInfo = deploymentInfo(); + + registrar.execute(null, deploymentInfo, config); + + verify(mcpSyncServer, times(1)).addTool(any()); + } + + private ElementProperties elementProperties(String elementType) { + Map props = new HashMap<>(); + props.put(ChainProperties.ELEMENT_TYPE, elementType); + props.put(ChainProperties.ELEMENT_ID, ELEMENT_ID); + return ElementProperties.builder() + .elementId(ELEMENT_ID) + .properties(props) + .build(); + } + + private ElementProperties mcpTriggerProperties(String outputSchema) { + Map props = new HashMap<>(); + props.put(ChainProperties.ELEMENT_TYPE, "mcp-trigger"); + props.put(ChainProperties.ELEMENT_ID, ELEMENT_ID); + props.put("name", TOOL_NAME); + props.put("description", TOOL_DESCRIPTION); + props.put("title", TOOL_TITLE); + props.put("readOnly", "true"); + props.put("destructive", "false"); + props.put("idempotent", "true"); + props.put("openWorld", "false"); + props.put("requiresLocal", "false"); + props.put("inputSchema", INPUT_SCHEMA); + if (outputSchema != null) { + props.put("outputSchema", outputSchema); + } + return ElementProperties.builder() + .elementId(ELEMENT_ID) + .properties(props) + .build(); + } + + private DeploymentInfo deploymentInfo() { + return DeploymentInfo.builder() + .deploymentId(DEPLOYMENT_ID) + .chainId("chain-1") + .build(); + } +} diff --git a/src/test/java/org/qubership/integration/platform/engine/service/deployment/processing/actions/context/stop/McpToolUnregisterActionTest.java b/src/test/java/org/qubership/integration/platform/engine/service/deployment/processing/actions/context/stop/McpToolUnregisterActionTest.java new file mode 100644 index 00000000..b9470910 --- /dev/null +++ b/src/test/java/org/qubership/integration/platform/engine/service/deployment/processing/actions/context/stop/McpToolUnregisterActionTest.java @@ -0,0 +1,94 @@ +package org.qubership.integration.platform.engine.service.deployment.processing.actions.context.stop; + +import io.modelcontextprotocol.server.McpSyncServer; +import io.modelcontextprotocol.spec.McpSchema; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.qubership.integration.platform.engine.model.deployment.update.DeploymentConfiguration; +import org.qubership.integration.platform.engine.model.deployment.update.DeploymentInfo; + +import java.util.List; +import java.util.Map; + +import static org.mockito.Mockito.*; +import static org.qubership.integration.platform.engine.service.deployment.processing.actions.context.create.McpToolRegistrar.DEPLOYMENT_ID; + +class McpToolUnregisterActionTest { + + private McpSyncServer mcpSyncServer; + private McpToolUnregisterAction action; + + @BeforeEach + void setUp() { + mcpSyncServer = mock(McpSyncServer.class); + action = new McpToolUnregisterAction(mcpSyncServer); + } + + @Test + void executeRemovesToolsMatchingDeploymentId() { + String deploymentId = "deploy-1"; + McpSchema.Tool matchingTool = tool("tool-a", deploymentId); + when(mcpSyncServer.listTools()).thenReturn(List.of(matchingTool)); + + action.execute(null, deploymentInfo(deploymentId), null); + + verify(mcpSyncServer).removeTool("tool-a"); + } + + @Test + void executeDoesNotRemoveToolsFromOtherDeployments() { + McpSchema.Tool otherTool = tool("tool-b", "deploy-other"); + when(mcpSyncServer.listTools()).thenReturn(List.of(otherTool)); + + action.execute(null, deploymentInfo("deploy-1"), null); + + verify(mcpSyncServer, never()).removeTool(any()); + } + + @Test + void executeDoesNothingWhenNoToolsPresent() { + when(mcpSyncServer.listTools()).thenReturn(List.of()); + + action.execute(null, deploymentInfo("deploy-1"), null); + + verify(mcpSyncServer, never()).removeTool(any()); + } + + @Test + void executeRemovesOnlyMatchingToolsWhenMultiplePresent() { + String deploymentId = "deploy-1"; + McpSchema.Tool matchingTool1 = tool("tool-x", deploymentId); + McpSchema.Tool matchingTool2 = tool("tool-y", deploymentId); + McpSchema.Tool otherTool = tool("tool-z", "deploy-other"); + when(mcpSyncServer.listTools()).thenReturn(List.of(matchingTool1, matchingTool2, otherTool)); + + action.execute(null, deploymentInfo(deploymentId), null); + + verify(mcpSyncServer).removeTool("tool-x"); + verify(mcpSyncServer).removeTool("tool-y"); + verify(mcpSyncServer, never()).removeTool("tool-z"); + } + + @Test + void executeDoesNothingWhenDeploymentConfigurationIsNull() { + when(mcpSyncServer.listTools()).thenReturn(List.of()); + + action.execute(null, deploymentInfo("deploy-1"), (DeploymentConfiguration) null); + + verify(mcpSyncServer, never()).removeTool(any()); + } + + private McpSchema.Tool tool(String name, String deploymentId) { + return McpSchema.Tool.builder() + .name(name) + .meta(Map.of(DEPLOYMENT_ID, deploymentId)) + .inputSchema(new McpSchema.JsonSchema("object", null, null, null, null, null)) + .build(); + } + + private DeploymentInfo deploymentInfo(String deploymentId) { + return DeploymentInfo.builder() + .deploymentId(deploymentId) + .build(); + } +} From 34d86d549f9b5af18facc7611ae99809f3420a43 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:45:51 +0300 Subject: [PATCH 3/5] feat: added unit tests for JsonMessageValidator class --- .../engine/camel/JsonMessageValidator.java | 2 +- .../camel/JsonMessageValidatorTest.java | 106 ++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/qubership/integration/platform/engine/camel/JsonMessageValidatorTest.java diff --git a/src/main/java/org/qubership/integration/platform/engine/camel/JsonMessageValidator.java b/src/main/java/org/qubership/integration/platform/engine/camel/JsonMessageValidator.java index 074f5ae7..3d8317bc 100644 --- a/src/main/java/org/qubership/integration/platform/engine/camel/JsonMessageValidator.java +++ b/src/main/java/org/qubership/integration/platform/engine/camel/JsonMessageValidator.java @@ -28,7 +28,7 @@ @Component public class JsonMessageValidator { public static final String MESSAGE_VALIDATION_ERROR = "Errors during message validation: "; - private static final String EMPTY_BODY_ERROR = "Message body is empty"; + public static final String EMPTY_BODY_ERROR = "Message body is empty"; public void validate(String jsonMessageAsString, String jsonSchemaAsString) { SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7); diff --git a/src/test/java/org/qubership/integration/platform/engine/camel/JsonMessageValidatorTest.java b/src/test/java/org/qubership/integration/platform/engine/camel/JsonMessageValidatorTest.java new file mode 100644 index 00000000..f80aa918 --- /dev/null +++ b/src/test/java/org/qubership/integration/platform/engine/camel/JsonMessageValidatorTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2024-2025 NetCracker Technology Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.qubership.integration.platform.engine.camel; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.qubership.integration.platform.engine.errorhandling.ValidationException; + +import static org.junit.jupiter.api.Assertions.*; + +class JsonMessageValidatorTest { + + private static final String SCHEMA_STRING_FIELD = """ + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": ["name"] + } + """; + + private JsonMessageValidator validator; + + @BeforeEach + void setUp() { + validator = new JsonMessageValidator(); + } + + @Test + void validMessageDoesNotThrow() { + assertDoesNotThrow(() -> validator.validate("{\"name\": \"Alice\"}", SCHEMA_STRING_FIELD)); + } + + @Test + void nullMessageThrowsValidationException() { + ValidationException ex = assertThrows(ValidationException.class, + () -> validator.validate(null, SCHEMA_STRING_FIELD)); + assertEquals(JsonMessageValidator.EMPTY_BODY_ERROR, ex.getMessage()); + } + + @Test + void blankMessageThrowsValidationException() { + ValidationException ex = assertThrows(ValidationException.class, + () -> validator.validate(" ", SCHEMA_STRING_FIELD)); + assertEquals(JsonMessageValidator.EMPTY_BODY_ERROR, ex.getMessage()); + } + + @Test + void emptyStringMessageThrowsValidationException() { + ValidationException ex = assertThrows(ValidationException.class, + () -> validator.validate("", SCHEMA_STRING_FIELD)); + assertEquals(JsonMessageValidator.EMPTY_BODY_ERROR, ex.getMessage()); + } + + @Test + void messageFailingSchemaValidationThrowsValidationException() { + ValidationException ex = assertThrows(ValidationException.class, + () -> validator.validate("{\"name\": 123}", SCHEMA_STRING_FIELD)); + assertTrue(ex.getMessage().startsWith(JsonMessageValidator.MESSAGE_VALIDATION_ERROR)); + } + + @Test + void messageMissingRequiredFieldThrowsValidationException() { + ValidationException ex = assertThrows(ValidationException.class, + () -> validator.validate("{\"age\": 30}", SCHEMA_STRING_FIELD)); + assertTrue(ex.getMessage().startsWith(JsonMessageValidator.MESSAGE_VALIDATION_ERROR)); + } + + @Test + void multipleValidationErrorsAreAllIncludedInMessage() { + String schemaMultiRequired = """ + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer" } + }, + "required": ["name", "age"] + } + """; + + ValidationException ex = assertThrows(ValidationException.class, + () -> validator.validate("{}", schemaMultiRequired)); + String message = ex.getMessage(); + assertTrue(message.startsWith(JsonMessageValidator.MESSAGE_VALIDATION_ERROR)); + // Both missing fields should be reported + assertTrue(message.contains("name") || message.contains("age")); + } +} From 94f757068937e89c0c938e93c0faf3364efc7ae1 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:54:06 +0300 Subject: [PATCH 4/5] feat: removed commented out dependencies from pom.xml --- pom.xml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pom.xml b/pom.xml index 3375e7f6..c198db79 100644 --- a/pom.xml +++ b/pom.xml @@ -58,8 +58,6 @@ 1.5.5.Final 0.2.0 1.18.42 - - 19.0.3 3.25.8 5.20.0 @@ -213,12 +211,6 @@ ${atlasmap.version} - - - - - - io.kubernetes client-java @@ -593,10 +585,6 @@ org.springframework spring-webflux - - - - io.kubernetes From 64dfefe213ed34d07c7c82ef0698a968b62fc632 Mon Sep 17 00:00:00 2001 From: Sergei Skuratovich <900852+SSNikolaevich@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:08:08 +0300 Subject: [PATCH 5/5] feat: replaced 3 tests by parameterized one --- pom.xml | 6 +++++ .../camel/JsonMessageValidatorTest.java | 25 ++++++------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index c198db79..05f96012 100644 --- a/pom.xml +++ b/pom.xml @@ -383,6 +383,12 @@ test + + org.junit.jupiter + junit-jupiter-params + test + + org.mockito mockito-core diff --git a/src/test/java/org/qubership/integration/platform/engine/camel/JsonMessageValidatorTest.java b/src/test/java/org/qubership/integration/platform/engine/camel/JsonMessageValidatorTest.java index f80aa918..5295ba22 100644 --- a/src/test/java/org/qubership/integration/platform/engine/camel/JsonMessageValidatorTest.java +++ b/src/test/java/org/qubership/integration/platform/engine/camel/JsonMessageValidatorTest.java @@ -18,6 +18,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; import org.qubership.integration.platform.engine.errorhandling.ValidationException; import static org.junit.jupiter.api.Assertions.*; @@ -47,24 +50,12 @@ void validMessageDoesNotThrow() { assertDoesNotThrow(() -> validator.validate("{\"name\": \"Alice\"}", SCHEMA_STRING_FIELD)); } - @Test - void nullMessageThrowsValidationException() { - ValidationException ex = assertThrows(ValidationException.class, - () -> validator.validate(null, SCHEMA_STRING_FIELD)); - assertEquals(JsonMessageValidator.EMPTY_BODY_ERROR, ex.getMessage()); - } - - @Test - void blankMessageThrowsValidationException() { - ValidationException ex = assertThrows(ValidationException.class, - () -> validator.validate(" ", SCHEMA_STRING_FIELD)); - assertEquals(JsonMessageValidator.EMPTY_BODY_ERROR, ex.getMessage()); - } - - @Test - void emptyStringMessageThrowsValidationException() { + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void blankOrEmptyMessageThrowsValidationException(String message) { ValidationException ex = assertThrows(ValidationException.class, - () -> validator.validate("", SCHEMA_STRING_FIELD)); + () -> validator.validate(message, SCHEMA_STRING_FIELD)); assertEquals(JsonMessageValidator.EMPTY_BODY_ERROR, ex.getMessage()); }