diff --git a/pom.xml b/pom.xml
index 92080ba5..5009dbd1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,6 +49,7 @@
4.14.6
2025.0.1
+ 1.1.3
1.1.10.8
3.7.4
2.5.2
@@ -57,7 +58,6 @@
1.5.5.Final
0.2.0
1.18.42
- 1.0.87
19.0.3
3.25.8
5.20.0
@@ -121,6 +121,14 @@
import
+
+ org.springframework.ai
+ spring-ai-bom
+ ${spring-ai.version}
+ pom
+ import
+
+
@@ -203,12 +211,6 @@
${atlasmap.version}
-
- com.networknt
- json-schema-validator
- ${json-schema-validator.version}
-
-
io.kubernetes
client-java
@@ -387,6 +389,18 @@
test
+
+ org.junit.jupiter
+ junit-jupiter-params
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
de.siegmar
logback-gelf
@@ -583,10 +597,6 @@
org.springframework
spring-webflux
-
- com.networknt
- json-schema-validator
-
io.kubernetes
@@ -662,6 +672,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..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
@@ -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 static final String EMPTY_BODY_ERROR = "Message body is empty";
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 b7a50e79..e5465878 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
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..5295ba22
--- /dev/null
+++ b/src/test/java/org/qubership/integration/platform/engine/camel/JsonMessageValidatorTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.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.*;
+
+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));
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ @ValueSource(strings = {" "})
+ void blankOrEmptyMessageThrowsValidationException(String message) {
+ ValidationException ex = assertThrows(ValidationException.class,
+ () -> validator.validate(message, 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"));
+ }
+}
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();
+ }
+}