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());
}