Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 25 additions & 10 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@

<camel.version>4.14.5</camel.version>
<spring-cloud.version>2025.0.1</spring-cloud.version>
<spring-ai.version>1.1.3</spring-ai.version>
<snappy-java.version>1.1.10.8</snappy-java.version>
<hypersistence-utils-hibernate.version>3.7.4</hypersistence-utils-hibernate.version>
<atlasmap.version>2.5.2</atlasmap.version>
Expand All @@ -57,7 +58,8 @@
<mapstruct.version>1.5.5.Final</mapstruct.version>
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
<lombok.version>1.18.42</lombok.version>
<json-schema-validator.version>1.0.87</json-schema-validator.version>
<!-- <json-schema-validator.version>3.0.1</json-schema-validator.version>-->
<!-- <json-schema-validator.version>2.0.0</json-schema-validator.version>-->

Check warning on line 62 in pom.xml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this commented out code.

See more on https://sonarcloud.io/project/issues?id=Netcracker_qubership-integration-engine&issues=AZ0-gEebbKglt3FM1Pza&open=AZ0-gEebbKglt3FM1Pza&pullRequest=488
<kubernetes-client-java.version>19.0.3</kubernetes-client-java.version>
<protobuf-java.version>3.25.8</protobuf-java.version>
<amqp-client.version>5.20.0</amqp-client.version>
Expand Down Expand Up @@ -121,6 +123,14 @@
<scope>import</scope>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!-- Dependencies -->

<dependency>
Expand Down Expand Up @@ -203,11 +213,11 @@
<version>${atlasmap.version}</version>
</dependency>

<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>${json-schema-validator.version}</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.networknt</groupId>-->
<!-- <artifactId>json-schema-validator</artifactId>-->
<!-- <version>${json-schema-validator.version}</version>-->
<!-- </dependency>-->

Check warning on line 220 in pom.xml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this commented out code.

See more on https://sonarcloud.io/project/issues?id=Netcracker_qubership-integration-engine&issues=AZ0-gEebbKglt3FM1Pzb&open=AZ0-gEebbKglt3FM1Pzb&pullRequest=488

<dependency>
<groupId>io.kubernetes</groupId>
Expand Down Expand Up @@ -577,10 +587,10 @@
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.networknt</groupId>-->
<!-- <artifactId>json-schema-validator</artifactId>-->
<!-- </dependency>-->

Check warning on line 593 in pom.xml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this commented out code.

See more on https://sonarcloud.io/project/issues?id=Netcracker_qubership-integration-engine&issues=AZ0-gEebbKglt3FM1Pzc&open=AZ0-gEebbKglt3FM1Pzc&pullRequest=488

<dependency>
<groupId>io.kubernetes</groupId>
Expand Down Expand Up @@ -656,6 +666,11 @@
<groupId>at.yawk.lz4</groupId>
<artifactId>lz4-java</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ValidationMessage> 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<Error> 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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<McpSchema.Tool> toolsToRemove = mcpSyncServer.listTools()
.stream()
.filter(tool -> deploymentInfo.getDeploymentId()
.equals(tool.meta().get(DEPLOYMENT_ID)))
.toList();
toolsToRemove.forEach(tool -> mcpSyncServer.removeTool(tool.name()));
}
}
16 changes: 16 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading