From 98ccd6e3b4d8ca7cd643e9121349c5de8a47cc37 Mon Sep 17 00:00:00 2001 From: jerryyummy <461697582@qq.com> Date: Tue, 1 Jul 2025 11:38:40 +0800 Subject: [PATCH 01/36] basic arch --- .../eventmesh-connector-mcp/build.gradle | 19 +++++++++++ .../eventmesh-connector-mcp/gradle.properties | 18 ++++++++++ .../adapter/EventMeshDispatcherAdapter.java | 5 +++ .../connector/mcp/config/McpServerConfig.java | 33 +++++++++++++++++++ .../mcp/handler/AbstractMcpHandler.java | 5 +++ .../mcp/handler/McpDeliveryStrategy.java | 5 +++ .../connector/mcp/handler/McpHandler.java | 5 +++ .../handler/impl/McpHandlerDispatcher.java | 5 +++ .../handler/impl/SessionRecoverHandler.java | 5 +++ .../mcp/handler/impl/StreamMcpHandler.java | 5 +++ .../connector/mcp/protocol/McpConstants.java | 5 +++ .../connector/mcp/protocol/Protocol.java | 5 +++ .../mcp/protocol/ProtocolFactory.java | 5 +++ .../mcp/protocol/impl/McpRequest.java | 5 +++ .../mcp/protocol/impl/McpResponse.java | 5 +++ .../protocol/impl/McpStandardProtocol.java | 5 +++ .../connector/mcp/server/McpServer.java | 5 +++ .../mcp/server/McpServerInitializer.java | 5 +++ .../connector/mcp/session/McpSession.java | 5 +++ .../mcp/session/McpSessionManager.java | 5 +++ .../connector/mcp/util/McpUtils.java | 4 +++ ...esh.openconnect.api.ConnectorCreateService | 19 +++++++++++ .../src/main/resources/server-config.yml | 19 +++++++++++ .../src/test/resources/server-config.yml | 19 +++++++++++ .../runtime/boot/EventMeshStartup.java | 3 +- settings.gradle | 1 + 26 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/build.gradle create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/gradle.properties create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/adapter/EventMeshDispatcherAdapter.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/config/McpServerConfig.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/AbstractMcpHandler.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/McpDeliveryStrategy.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/McpHandler.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/McpHandlerDispatcher.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/SessionRecoverHandler.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/StreamMcpHandler.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/McpConstants.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/Protocol.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/ProtocolFactory.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpRequest.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpResponse.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpStandardProtocol.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpServer.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpServerInitializer.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSession.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSessionManager.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/McpUtils.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/META-INF.eventmesh/org.apache.eventmesh.openconnect.api.ConnectorCreateService create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/server-config.yml create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/test/resources/server-config.yml diff --git a/eventmesh-connectors/eventmesh-connector-mcp/build.gradle b/eventmesh-connectors/eventmesh-connector-mcp/build.gradle new file mode 100644 index 0000000000..84a228a28d --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java' +} + +group = 'org.apache.eventmesh' +version = '1.11.0-release' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/gradle.properties b/eventmesh-connectors/eventmesh-connector-mcp/gradle.properties new file mode 100644 index 0000000000..5e98eb968e --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/gradle.properties @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# +pluginType=connector +pluginName=mcp \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/adapter/EventMeshDispatcherAdapter.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/adapter/EventMeshDispatcherAdapter.java new file mode 100644 index 0000000000..f3e3dc04ef --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/adapter/EventMeshDispatcherAdapter.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.adapter; + +// 将 MCP 请求转为内部 EventMesh 消息结构(如需投递) +public class EventMeshDispatcherAdapter { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/config/McpServerConfig.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/config/McpServerConfig.java new file mode 100644 index 0000000000..fc9bfd489b --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/config/McpServerConfig.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.config; + +import org.apache.eventmesh.common.config.connector.Config; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class McpServerConfig extends Config { + + private boolean sourceEnable; + + private boolean sinkEnable; +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/AbstractMcpHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/AbstractMcpHandler.java new file mode 100644 index 0000000000..16cb928354 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/AbstractMcpHandler.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.handler; + +// MCP 请求统一入口抽象定义 +public class AbstractMcpHandler { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/McpDeliveryStrategy.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/McpDeliveryStrategy.java new file mode 100644 index 0000000000..4a3645851d --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/McpDeliveryStrategy.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.handler; + +// 响应策略接口(stream, once, complete等) +public class McpDeliveryStrategy { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/McpHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/McpHandler.java new file mode 100644 index 0000000000..2b24b95e21 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/McpHandler.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.handler; + +// Handler接口(或类)定义 +public class McpHandler { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/McpHandlerDispatcher.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/McpHandlerDispatcher.java new file mode 100644 index 0000000000..b02ce83f72 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/McpHandlerDispatcher.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.handler.impl; + +// 根据 path 分发到不同 handler +public class McpHandlerDispatcher { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/SessionRecoverHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/SessionRecoverHandler.java new file mode 100644 index 0000000000..5b4052265e --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/SessionRecoverHandler.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.handler.impl; + +// 处理会话恢复 +public class SessionRecoverHandler { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/StreamMcpHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/StreamMcpHandler.java new file mode 100644 index 0000000000..50b1be76bf --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/StreamMcpHandler.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.handler.impl; + +// 支持 stream 响应 +public class StreamMcpHandler { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/McpConstants.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/McpConstants.java new file mode 100644 index 0000000000..104014c864 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/McpConstants.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.protocol; + +// header key、content-type 常量等 +public class McpConstants { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/Protocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/Protocol.java new file mode 100644 index 0000000000..6b4f7c8ee8 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/Protocol.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.protocol; + +// # 抽象协议解析器接口 +public class Protocol { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/ProtocolFactory.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/ProtocolFactory.java new file mode 100644 index 0000000000..44f3263d54 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/ProtocolFactory.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.protocol; + +// 创建具体协议实现类 +public class ProtocolFactory { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpRequest.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpRequest.java new file mode 100644 index 0000000000..c6aff311b5 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpRequest.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.protocol.impl; + +// POJO: inputs, metadata, context +public class McpRequest { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpResponse.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpResponse.java new file mode 100644 index 0000000000..326f338fd6 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpResponse.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.protocol.impl; + +// POJO: outputs, finish_reason +public class McpResponse { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpStandardProtocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpStandardProtocol.java new file mode 100644 index 0000000000..d11f565e23 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpStandardProtocol.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.protocol.impl; + +// 解析标准 2025-03-26 spec 请求 +public class McpStandardProtocol { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpServer.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpServer.java new file mode 100644 index 0000000000..961fea4ec7 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpServer.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.server; + +// Netty Bootstrap +public class McpServer { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpServerInitializer.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpServerInitializer.java new file mode 100644 index 0000000000..cf7813b6d6 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpServerInitializer.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.server; + +// 配置 pipeline: codec, aggregator, handlers +public class McpServerInitializer { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSession.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSession.java new file mode 100644 index 0000000000..397988ebaa --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSession.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.session; + +// 会话对象(上下文、历史消息等) +public class McpSession { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSessionManager.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSessionManager.java new file mode 100644 index 0000000000..8defefd6f5 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSessionManager.java @@ -0,0 +1,5 @@ +package org.apache.eventmesh.connector.mcp.session; + +// 管理会话Map/Redis +public class McpSessionManager { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/McpUtils.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/McpUtils.java new file mode 100644 index 0000000000..11db79c7a8 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/McpUtils.java @@ -0,0 +1,4 @@ +package org.apache.eventmesh.connector.mcp.util; + +public class McpUtils { +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/META-INF.eventmesh/org.apache.eventmesh.openconnect.api.ConnectorCreateService b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/META-INF.eventmesh/org.apache.eventmesh.openconnect.api.ConnectorCreateService new file mode 100644 index 0000000000..bde014808a --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/META-INF.eventmesh/org.apache.eventmesh.openconnect.api.ConnectorCreateService @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + + +MCP-Source=org.apache.eventmesh.connector.mcp.source.McpSourceConnector diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/server-config.yml b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/server-config.yml new file mode 100644 index 0000000000..0cd7b5b5ab --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/server-config.yml @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +sourceEnable: true +sinkEnable: false diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/test/resources/server-config.yml b/eventmesh-connectors/eventmesh-connector-mcp/src/test/resources/server-config.yml new file mode 100644 index 0000000000..0cd7b5b5ab --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/test/resources/server-config.yml @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +sourceEnable: true +sinkEnable: false diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshStartup.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshStartup.java index 5c6ae90aae..a357ec5eee 100644 --- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshStartup.java +++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshStartup.java @@ -31,7 +31,8 @@ public class EventMeshStartup { public static void main(String[] args) throws Exception { try { ConfigService.getInstance() - .setConfigPath(EventMeshConstants.EVENTMESH_CONF_HOME + File.separator) + //.setConfigPath(EventMeshConstants.EVENTMESH_CONF_HOME + File.separator) + .setConfigPath("eventmesh-runtime/conf") .setRootConfig(EventMeshConstants.EVENTMESH_CONF_FILE); EventMeshServer server = new EventMeshServer(); diff --git a/settings.gradle b/settings.gradle index b013a57929..327ca7e1a2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -76,6 +76,7 @@ include 'eventmesh-connectors:eventmesh-connector-wechat' include 'eventmesh-connectors:eventmesh-connector-http' include 'eventmesh-connectors:eventmesh-connector-chatgpt' include 'eventmesh-connectors:eventmesh-connector-canal' +include 'eventmesh-connectors:eventmesh-connector-mcp' include 'eventmesh-storage-plugin:eventmesh-storage-api' include 'eventmesh-storage-plugin:eventmesh-storage-standalone' From 69793a52f46f114406306cf63cada5090149e38f Mon Sep 17 00:00:00 2001 From: "youyun.0601" Date: Tue, 1 Jul 2025 21:55:10 +0800 Subject: [PATCH 02/36] refine --- .../adapter/EventMeshDispatcherAdapter.java | 5 - .../mcp/handler/AbstractMcpHandler.java | 5 - .../mcp/handler/McpDeliveryStrategy.java | 5 - .../connector/mcp/handler/McpHandler.java | 5 - .../handler/impl/McpHandlerDispatcher.java | 5 - .../handler/impl/SessionRecoverHandler.java | 5 - .../mcp/handler/impl/StreamMcpHandler.java | 5 - .../connector/mcp/protocol/Protocol.java | 5 - .../mcp/protocol/ProtocolFactory.java | 5 - .../mcp/protocol/impl/McpRequest.java | 5 - .../mcp/protocol/impl/McpResponse.java | 5 - .../protocol/impl/McpStandardProtocol.java | 5 - .../mcp/server/McpConnectServer.java | 17 ++ .../connector/mcp/server/McpServer.java | 5 - .../mcp/server/McpServerInitializer.java | 5 - .../connector/mcp/sink/McpSinkConnector.java | 174 ++++++++++++ .../mcp/sink/data/McpAttemptEvent.java | 122 ++++++++ .../mcp/sink/data/McpConnectRecord.java | 117 ++++++++ .../mcp/sink/data/McpExportMetadata.java | 48 ++++ .../mcp/sink/data/McpExportRecord.java | 37 +++ .../mcp/sink/data/McpExportRecordPage.java | 42 +++ .../mcp/sink/data/MultiMcpRequestContext.java | 74 +++++ .../sink/handler/AbstractMcpSinkHandler.java | 101 +++++++ .../mcp/sink/handler/McpDeliveryStrategy.java | 23 ++ .../mcp/sink/handler/McpSinkHandler.java | 75 +++++ .../handler/impl/CommonMcpSinkHandler.java | 268 ++++++++++++++++++ .../impl/McpSinkHandlerRetryWrapper.java | 118 ++++++++ .../mcp/source/McpSourceConnector.java | 216 ++++++++++++++ .../connector/mcp/source/data/McpRequest.java | 32 +++ .../mcp/source/data/McpResponse.java | 44 +++ .../{ => source}/protocol/McpConstants.java | 2 +- .../mcp/source/protocol/Protocol.java | 58 ++++ .../mcp/source/protocol/ProtocolFactory.java | 58 ++++ .../protocol/impl/McpStandardProtocol.java | 119 ++++++++ .../src/main/resources/source-config.yml | 42 +++ .../eventmesh-protocol-mcp/build.gradle | 27 ++ .../eventmesh-protocol-mcp/gradle.porperties | 18 ++ .../protocol/mcp/McpProtocolAdaptor.java | 117 ++++++++ .../protocol/mcp/McpProtocolConstant.java | 42 +++ .../resolver/McpRequestProtocolResolver.java | 104 +++++++ .../protocol/mcp/session/McpSession.java | 4 + .../mcp/session/McpSessionManager.java | 4 + ...che.eventmesh.protocol.mcp.ProtocolAdaptor | 16 ++ .../runtime/boot/EventMeshStartup.java | 3 +- 44 files changed, 2119 insertions(+), 73 deletions(-) delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/adapter/EventMeshDispatcherAdapter.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/AbstractMcpHandler.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/McpDeliveryStrategy.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/McpHandler.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/McpHandlerDispatcher.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/SessionRecoverHandler.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/StreamMcpHandler.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/Protocol.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/ProtocolFactory.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpRequest.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpResponse.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpStandardProtocol.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpServer.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpServerInitializer.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpAttemptEvent.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/MultiMcpRequestContext.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpDeliveryStrategy.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java rename eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/{ => source}/protocol/McpConstants.java (53%) create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/ProtocolFactory.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/source-config.yml create mode 100644 eventmesh-protocol-plugin/eventmesh-protocol-mcp/build.gradle create mode 100644 eventmesh-protocol-plugin/eventmesh-protocol-mcp/gradle.porperties create mode 100644 eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolAdaptor.java create mode 100644 eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolConstant.java create mode 100644 eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/resolver/McpRequestProtocolResolver.java create mode 100644 eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/session/McpSession.java create mode 100644 eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/session/McpSessionManager.java create mode 100644 eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/resources/eventmesh/org.apache.eventmesh.protocol.mcp.ProtocolAdaptor diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/adapter/EventMeshDispatcherAdapter.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/adapter/EventMeshDispatcherAdapter.java deleted file mode 100644 index f3e3dc04ef..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/adapter/EventMeshDispatcherAdapter.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.apache.eventmesh.connector.mcp.adapter; - -// 将 MCP 请求转为内部 EventMesh 消息结构(如需投递) -public class EventMeshDispatcherAdapter { -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/AbstractMcpHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/AbstractMcpHandler.java deleted file mode 100644 index 16cb928354..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/AbstractMcpHandler.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.apache.eventmesh.connector.mcp.handler; - -// MCP 请求统一入口抽象定义 -public class AbstractMcpHandler { -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/McpDeliveryStrategy.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/McpDeliveryStrategy.java deleted file mode 100644 index 4a3645851d..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/McpDeliveryStrategy.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.apache.eventmesh.connector.mcp.handler; - -// 响应策略接口(stream, once, complete等) -public class McpDeliveryStrategy { -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/McpHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/McpHandler.java deleted file mode 100644 index 2b24b95e21..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/McpHandler.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.apache.eventmesh.connector.mcp.handler; - -// Handler接口(或类)定义 -public class McpHandler { -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/McpHandlerDispatcher.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/McpHandlerDispatcher.java deleted file mode 100644 index b02ce83f72..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/McpHandlerDispatcher.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.apache.eventmesh.connector.mcp.handler.impl; - -// 根据 path 分发到不同 handler -public class McpHandlerDispatcher { -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/SessionRecoverHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/SessionRecoverHandler.java deleted file mode 100644 index 5b4052265e..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/SessionRecoverHandler.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.apache.eventmesh.connector.mcp.handler.impl; - -// 处理会话恢复 -public class SessionRecoverHandler { -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/StreamMcpHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/StreamMcpHandler.java deleted file mode 100644 index 50b1be76bf..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/handler/impl/StreamMcpHandler.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.apache.eventmesh.connector.mcp.handler.impl; - -// 支持 stream 响应 -public class StreamMcpHandler { -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/Protocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/Protocol.java deleted file mode 100644 index 6b4f7c8ee8..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/Protocol.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.apache.eventmesh.connector.mcp.protocol; - -// # 抽象协议解析器接口 -public class Protocol { -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/ProtocolFactory.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/ProtocolFactory.java deleted file mode 100644 index 44f3263d54..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/ProtocolFactory.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.apache.eventmesh.connector.mcp.protocol; - -// 创建具体协议实现类 -public class ProtocolFactory { -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpRequest.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpRequest.java deleted file mode 100644 index c6aff311b5..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.apache.eventmesh.connector.mcp.protocol.impl; - -// POJO: inputs, metadata, context -public class McpRequest { -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpResponse.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpResponse.java deleted file mode 100644 index 326f338fd6..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.apache.eventmesh.connector.mcp.protocol.impl; - -// POJO: outputs, finish_reason -public class McpResponse { -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpStandardProtocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpStandardProtocol.java deleted file mode 100644 index d11f565e23..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/impl/McpStandardProtocol.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.apache.eventmesh.connector.mcp.protocol.impl; - -// 解析标准 2025-03-26 spec 请求 -public class McpStandardProtocol { -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java new file mode 100644 index 0000000000..d3eb1e733d --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java @@ -0,0 +1,17 @@ +package org.apache.eventmesh.connector.mcp.server; + +import org.apache.eventmesh.connector.mcp.config.McpServerConfig; +import org.apache.eventmesh.connector.mcp.source.McpSourceConnector; +import org.apache.eventmesh.openconnect.Application; +import org.apache.eventmesh.openconnect.util.ConfigUtil; + +public class McpConnectServer { + public static void main(String[] args) throws Exception { + McpServerConfig serverConfig = ConfigUtil.parse(McpServerConfig.class, "server-config.yml"); + + if (serverConfig.isSourceEnable()) { + Application mcpSourceApp = new Application(); + mcpSourceApp.run(McpSourceConnector.class); + } + } +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpServer.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpServer.java deleted file mode 100644 index 961fea4ec7..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpServer.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.apache.eventmesh.connector.mcp.server; - -// Netty Bootstrap -public class McpServer { -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpServerInitializer.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpServerInitializer.java deleted file mode 100644 index cf7813b6d6..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpServerInitializer.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.apache.eventmesh.connector.mcp.server; - -// 配置 pipeline: codec, aggregator, handlers -public class McpServerInitializer { -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java new file mode 100644 index 0000000000..adc218823b --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink; + +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.eventmesh.common.EventMeshThreadFactory; +import org.apache.eventmesh.common.config.connector.Config; +import org.apache.eventmesh.common.config.connector.http.HttpSinkConfig; +import org.apache.eventmesh.common.config.connector.http.SinkConnectorConfig; +import org.apache.eventmesh.connector.http.sink.handler.HttpSinkHandler; +import org.apache.eventmesh.connector.http.sink.handler.impl.CommonHttpSinkHandler; +import org.apache.eventmesh.connector.http.sink.handler.impl.HttpSinkHandlerRetryWrapper; +import org.apache.eventmesh.connector.http.sink.handler.impl.WebhookHttpSinkHandler; +import org.apache.eventmesh.openconnect.api.ConnectorCreateService; +import org.apache.eventmesh.openconnect.api.connector.ConnectorContext; +import org.apache.eventmesh.openconnect.api.connector.SinkConnectorContext; +import org.apache.eventmesh.openconnect.api.sink.Sink; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +@Slf4j +public class McpSinkConnector implements Sink, ConnectorCreateService { + + private HttpSinkConfig httpSinkConfig; + + @Getter + private HttpSinkHandler sinkHandler; + + private ThreadPoolExecutor executor; + + private final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(10000); + + private final AtomicBoolean isStart = new AtomicBoolean(true); + + @Override + public Class configClass() { + return HttpSinkConfig.class; + } + + @Override + public Sink create() { + return new McpSinkConnector(); + } + + @Override + public void init(Config config) throws Exception { + this.httpSinkConfig = (HttpSinkConfig) config; + doInit(); + } + + @Override + public void init(ConnectorContext connectorContext) throws Exception { + SinkConnectorContext sinkConnectorContext = (SinkConnectorContext) connectorContext; + this.httpSinkConfig = (HttpSinkConfig) sinkConnectorContext.getSinkConfig(); + doInit(); + } + + @SneakyThrows + private void doInit() { + // Fill default values if absent + SinkConnectorConfig.populateFieldsWithDefaults(this.httpSinkConfig.connectorConfig); + // Create different handlers for different configurations + HttpSinkHandler nonRetryHandler; + if (this.httpSinkConfig.connectorConfig.getWebhookConfig().isActivate()) { + nonRetryHandler = new WebhookHttpSinkHandler(this.httpSinkConfig.connectorConfig); + } else { + nonRetryHandler = new CommonHttpSinkHandler(this.httpSinkConfig.connectorConfig); + } + + int maxRetries = this.httpSinkConfig.connectorConfig.getRetryConfig().getMaxRetries(); + if (maxRetries == 0) { + // Use the original sink handler + this.sinkHandler = nonRetryHandler; + } else if (maxRetries > 0) { + // Wrap the sink handler with a retry handler + this.sinkHandler = new HttpSinkHandlerRetryWrapper(this.httpSinkConfig.connectorConfig, nonRetryHandler); + } else { + throw new IllegalArgumentException("Max retries must be greater than or equal to 0."); + } + boolean isParallelized = this.httpSinkConfig.connectorConfig.isParallelized(); + int parallelism = isParallelized ? this.httpSinkConfig.connectorConfig.getParallelism() : 1; + executor = new ThreadPoolExecutor(parallelism, parallelism, 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(), new EventMeshThreadFactory("http-sink-handler")); + } + + @Override + public void start() throws Exception { + this.sinkHandler.start(); + for (int i = 0; i < this.httpSinkConfig.connectorConfig.getParallelism(); i++) { + executor.execute(() -> { + while (isStart.get()) { + ConnectRecord connectRecord = null; + try { + connectRecord = queue.poll(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (connectRecord != null) { + sinkHandler.handle(connectRecord); + } + } + }); + } + } + + @Override + public void commit(ConnectRecord record) { + + } + + @Override + public String name() { + return this.httpSinkConfig.connectorConfig.getConnectorName(); + } + + @Override + public void onException(ConnectRecord record) { + + } + + @Override + public void stop() throws Exception { + isStart.set(false); + while (!queue.isEmpty()) { + ConnectRecord record = queue.poll(); + this.sinkHandler.handle(record); + } + try { + Thread.sleep(50); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + this.sinkHandler.stop(); + log.info("All tasks completed, start shut down http sink connector"); + } + + @Override + public void put(List sinkRecords) { + for (ConnectRecord sinkRecord : sinkRecords) { + try { + if (Objects.isNull(sinkRecord)) { + log.warn("ConnectRecord data is null, ignore."); + continue; + } + queue.put(sinkRecord); + } catch (Exception e) { + log.error("Failed to sink message via HTTP. ", e); + } + } + } +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpAttemptEvent.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpAttemptEvent.java new file mode 100644 index 0000000000..5697dff4b3 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpAttemptEvent.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Single HTTP attempt event + */ +public class McpAttemptEvent { + + public static final String PREFIX = "http-attempt-event-"; + + private final int maxAttempts; + + private final AtomicInteger attempts; + + private Throwable lastException; + + + public McpAttemptEvent(int maxAttempts) { + this.maxAttempts = maxAttempts; + this.attempts = new AtomicInteger(0); + } + + /** + * Increment the attempts + */ + public void incrementAttempts() { + attempts.incrementAndGet(); + } + + /** + * Update the event, incrementing the attempts and setting the last exception + * + * @param exception the exception to update, can be null + */ + public void updateEvent(Throwable exception) { + // increment the attempts + incrementAttempts(); + + // update the last exception + lastException = exception; + } + + /** + * Check if the attempts are less than the maximum attempts + * + * @return true if the attempts are less than the maximum attempts, false otherwise + */ + public boolean canAttempt() { + return attempts.get() < maxAttempts; + } + + public boolean isComplete() { + if (attempts.get() == 0) { + // No start yet + return false; + } + + // If no attempt can be made or the last exception is null, the event completed + return !canAttempt() || lastException == null; + } + + + public int getMaxAttempts() { + return maxAttempts; + } + + public int getAttempts() { + return attempts.get(); + } + + public Throwable getLastException() { + return lastException; + } + + /** + * Get the limited exception message with the default limit of 256 + * + * @return the limited exception message + */ + public String getLimitedExceptionMessage() { + return getLimitedExceptionMessage(256); + } + + /** + * Get the limited exception message with the specified limit + * + * @param maxLimit the maximum limit of the exception message + * @return the limited exception message + */ + public String getLimitedExceptionMessage(int maxLimit) { + if (lastException == null) { + return ""; + } + String message = lastException.getMessage(); + if (message == null) { + return ""; + } + if (message.length() > maxLimit) { + return message.substring(0, maxLimit); + } + return message; + } + +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java new file mode 100644 index 0000000000..1750fb7ba4 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; + +import lombok.Builder; +import lombok.Getter; +import org.apache.eventmesh.common.remote.offset.http.HttpRecordOffset; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.KeyValue; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * a special ConnectRecord for HttpSinkConnector + */ +@Getter +@Builder +public class McpConnectRecord implements Serializable { + + private static final long serialVersionUID = 5271462532332251473L; + + /** + * The unique identifier for the HttpConnectRecord + */ + private final String httpRecordId = UUID.randomUUID().toString(); + + /** + * The time when the HttpConnectRecord was created + */ + private LocalDateTime createTime; + + /** + * The type of the HttpConnectRecord + */ + private String type; + + /** + * The event id of the HttpConnectRecord + */ + private String eventId; + + private Object data; + + private KeyValue extensions; + + @Override + public String toString() { + return "HttpConnectRecord{" + + "createTime=" + createTime + + ", httpRecordId='" + httpRecordId + + ", type='" + type + + ", eventId='" + eventId + + ", data=" + data + + ", extensions=" + extensions + + '}'; + } + + /** + * Convert ConnectRecord to HttpConnectRecord + * + * @param record the ConnectRecord to convert + * @return the converted HttpConnectRecord + */ + public static McpConnectRecord convertConnectRecord(ConnectRecord record, String type) { + Map offsetMap = new HashMap<>(); + if (record != null && record.getPosition() != null && record.getPosition().getRecordOffset() != null) { + if (HttpRecordOffset.class.equals(record.getPosition().getRecordOffsetClazz())) { + offsetMap = ((HttpRecordOffset) record.getPosition().getRecordOffset()).getOffsetMap(); + } + } + String offset = "0"; + if (!offsetMap.isEmpty()) { + offset = offsetMap.values().iterator().next().toString(); + } + if (record.getData() instanceof byte[]) { + String data = Base64.getEncoder().encodeToString((byte[]) record.getData()); + record.addExtension("isBase64", true); + return McpConnectRecord.builder() + .type(type) + .createTime(LocalDateTime.now()) + .eventId(type + "-" + offset) + .data(data) + .extensions(record.getExtensions()) + .build(); + } else { + record.addExtension("isBase64", false); + return McpConnectRecord.builder() + .type(type) + .createTime(LocalDateTime.now()) + .eventId(type + "-" + offset) + .data(record.getData()) + .extensions(record.getExtensions()) + .build(); + } + } + +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java new file mode 100644 index 0000000000..d9129cc498 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; + +import lombok.Builder; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * Metadata for an HTTP export operation. + */ +@Data +@Builder +public class McpExportMetadata implements Serializable { + + private static final long serialVersionUID = 1121010466793041920L; + + private String url; + + private int code; + + private String message; + + private LocalDateTime receivedTime; + + private String recordId; + + private String retriedBy; + + private int retryNum; +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java new file mode 100644 index 0000000000..346e054b92 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.io.Serializable; + +/** + * Represents an HTTP export record containing metadata and data to be exported. + */ +@Data +@AllArgsConstructor +public class McpExportRecord implements Serializable { + + private static final long serialVersionUID = 6010283911452947157L; + + private McpExportMetadata metadata; + + private Object data; +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java new file mode 100644 index 0000000000..2e3287a245 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.apache.eventmesh.connector.http.sink.data.HttpExportRecord; + +import java.io.Serializable; +import java.util.List; + +/** + * Represents a page of HTTP export records. + */ +@Data +@AllArgsConstructor +public class McpExportRecordPage implements Serializable { + + private static final long serialVersionUID = 1143791658357035990L; + + private int pageNum; + + private int pageSize; + + private List pageItems; + +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/MultiMcpRequestContext.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/MultiMcpRequestContext.java new file mode 100644 index 0000000000..1d6ec226a1 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/MultiMcpRequestContext.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; + +import org.apache.eventmesh.connector.http.sink.data.HttpAttemptEvent; + +import java.util.concurrent.atomic.AtomicInteger; + + +/** + * Multi HTTP request context + */ +public class MultiMcpRequestContext { + + public static final String NAME = "multi-http-request-context"; + + /** + * The remaining requests to be processed. + */ + private final AtomicInteger remainingRequests; + + /** + * The last failed event. + * If retries occur but still fail, it will be logged, and only the last one will be retained. + */ + private HttpAttemptEvent lastFailedEvent; + + public MultiMcpRequestContext(int remainingEvents) { + this.remainingRequests = new AtomicInteger(remainingEvents); + } + + /** + * Decrement the remaining requests by 1. + */ + public void decrementRemainingRequests() { + remainingRequests.decrementAndGet(); + } + + /** + * Check if all requests have been processed. + * + * @return true if all requests have been processed, false otherwise. + */ + public boolean isAllRequestsProcessed() { + return remainingRequests.get() == 0; + } + + public int getRemainingRequests() { + return remainingRequests.get(); + } + + public HttpAttemptEvent getLastFailedEvent() { + return lastFailedEvent; + } + + public void setLastFailedEvent(HttpAttemptEvent lastFailedEvent) { + this.lastFailedEvent = lastFailedEvent; + } +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java new file mode 100644 index 0000000000..03ae01c9e5 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.handler; + +import lombok.Getter; +import org.apache.eventmesh.common.config.connector.http.SinkConnectorConfig; +import org.apache.eventmesh.connector.http.sink.data.HttpAttemptEvent; +import org.apache.eventmesh.connector.http.sink.data.HttpConnectRecord; +import org.apache.eventmesh.connector.http.sink.data.MultiHttpRequestContext; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * AbstractHttpSinkHandler is an abstract class that provides a base implementation for HttpSinkHandler. + */ +public abstract class AbstractMcpSinkHandler implements McpSinkHandler { + + @Getter + private final SinkConnectorConfig sinkConnectorConfig; + + @Getter + private final List urls; + + private final McpDeliveryStrategy deliveryStrategy; + + private int roundRobinIndex = 0; + + protected AbstractMcpSinkHandler(SinkConnectorConfig sinkConnectorConfig) { + this.sinkConnectorConfig = sinkConnectorConfig; + this.deliveryStrategy = McpDeliveryStrategy.valueOf(sinkConnectorConfig.getDeliveryStrategy()); + // Initialize URLs + String[] urlStrings = sinkConnectorConfig.getUrls(); + this.urls = Arrays.stream(urlStrings) + .map(URI::create) + .collect(Collectors.toList()); + } + + /** + * Processes a ConnectRecord by sending it over HTTP or HTTPS. This method should be called for each ConnectRecord that needs to be processed. + * + * @param record the ConnectRecord to process + */ + @Override + public void handle(ConnectRecord record) { + // build attributes + Map attributes = new ConcurrentHashMap<>(); + + switch (deliveryStrategy) { + case ROUND_ROBIN: + attributes.put(MultiHttpRequestContext.NAME, new MultiHttpRequestContext(1)); + URI url = urls.get(roundRobinIndex); + roundRobinIndex = (roundRobinIndex + 1) % urls.size(); + sendRecordToUrl(record, attributes, url); + break; + case BROADCAST: + attributes.put(MultiHttpRequestContext.NAME, new MultiHttpRequestContext(urls.size())); + // send the record to all URLs + urls.forEach(url0 -> sendRecordToUrl(record, attributes, url0)); + break; + default: + throw new IllegalArgumentException("Unknown delivery strategy: " + deliveryStrategy); + } + } + + private void sendRecordToUrl(ConnectRecord record, Map attributes, URI url) { + // convert ConnectRecord to HttpConnectRecord + String type = String.format("%s.%s.%s", + this.sinkConnectorConfig.getConnectorName(), url.getScheme(), + this.sinkConnectorConfig.getWebhookConfig().isActivate() ? "webhook" : "common"); + HttpConnectRecord httpConnectRecord = HttpConnectRecord.convertConnectRecord(record, type); + + // add AttemptEvent to the attributes + HttpAttemptEvent attemptEvent = new HttpAttemptEvent(this.sinkConnectorConfig.getRetryConfig().getMaxRetries() + 1); + attributes.put(HttpAttemptEvent.PREFIX + httpConnectRecord.getHttpRecordId(), attemptEvent); + + // deliver the record + deliver(url, httpConnectRecord, attributes, record); + } + +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpDeliveryStrategy.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpDeliveryStrategy.java new file mode 100644 index 0000000000..07cbbe3d46 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpDeliveryStrategy.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.handler; + +public enum McpDeliveryStrategy { + ROUND_ROBIN, + BROADCAST +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java new file mode 100644 index 0000000000..02d18e8d2e --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.handler; + +import io.vertx.core.Future; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.client.HttpResponse; +import org.apache.eventmesh.connector.http.sink.data.HttpConnectRecord; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; + +import java.net.URI; +import java.util.Map; + +/** + * Interface for handling ConnectRecords via HTTP or HTTPS. Classes implementing this interface are responsible for processing ConnectRecords by + * sending them over HTTP or HTTPS, with additional support for handling multiple requests and asynchronous processing. + * + *

Any class that needs to process ConnectRecords via HTTP or HTTPS should implement this interface. + * Implementing classes must provide implementations for the {@link #start()}, {@link #handle(ConnectRecord)}, + * {@link #deliver(URI, HttpConnectRecord, Map, ConnectRecord)}, and {@link #stop()} methods.

+ * + *

Implementing classes should ensure thread safety and handle HTTP/HTTPS communication efficiently. + * The {@link #start()} method initializes any necessary resources for HTTP/HTTPS communication. The {@link #handle(ConnectRecord)} method processes a + * ConnectRecord by sending it over HTTP or HTTPS. The {@link #deliver(URI, HttpConnectRecord, Map, ConnectRecord)} method processes HttpConnectRecord + * on specified URL while returning its own processing logic {@link #stop()} method releases any resources used for HTTP/HTTPS communication.

+ * + *

It's recommended to handle exceptions gracefully within the {@link #deliver(URI, HttpConnectRecord, Map, ConnectRecord)} method + * to prevent message loss or processing interruptions.

+ */ +public interface McpSinkHandler { + + /** + * Initializes the HTTP/HTTPS handler. This method should be called before using the handler. + */ + void start(); + + /** + * Processes a ConnectRecord by sending it over HTTP or HTTPS. This method should be called for each ConnectRecord that needs to be processed. + * + * @param record the ConnectRecord to process + */ + void handle(ConnectRecord record); + + + /** + * Processes HttpConnectRecord on specified URL while returning its own processing logic + * + * @param url URI to which the HttpConnectRecord should be sent + * @param httpConnectRecord HttpConnectRecord to process + * @param attributes additional attributes to be used in processing + * @return processing chain + */ + Future> deliver(URI url, HttpConnectRecord httpConnectRecord, Map attributes, ConnectRecord connectRecord); + + /** + * Cleans up and releases resources used by the HTTP/HTTPS handler. This method should be called when the handler is no longer needed. + */ + void stop(); +} + diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java new file mode 100644 index 0000000000..0f5e633e42 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java @@ -0,0 +1,268 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.handler.impl; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpHeaders; +import io.vertx.ext.web.client.HttpResponse; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.eventmesh.common.config.connector.http.SinkConnectorConfig; +import org.apache.eventmesh.common.utils.JsonUtils; +import org.apache.eventmesh.connector.http.sink.data.HttpAttemptEvent; +import org.apache.eventmesh.connector.http.sink.data.HttpConnectRecord; +import org.apache.eventmesh.connector.http.sink.data.MultiHttpRequestContext; +import org.apache.eventmesh.connector.http.sink.handler.AbstractHttpSinkHandler; +import org.apache.eventmesh.connector.http.util.HttpUtils; +import org.apache.eventmesh.openconnect.offsetmgmt.api.callback.SendExceptionContext; +import org.apache.eventmesh.openconnect.offsetmgmt.api.callback.SendResult; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; + +import java.net.URI; +import java.time.ZoneId; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Common HTTP/HTTPS Sink Handler implementation to handle ConnectRecords by sending them over HTTP or HTTPS to configured URLs. + * + *

This handler initializes a WebClient for making HTTP requests based on the provided SinkConnectorConfig. + * It handles processing ConnectRecords by converting them to HttpConnectRecord and sending them asynchronously to each configured URL using the + * WebClient.

+ * + *

The handler uses Vert.x's WebClient to perform HTTP/HTTPS requests. It initializes the WebClient in the {@link #start()} + * method and closes it in the {@link #stop()} method to manage resources efficiently.

+ * + *

Each ConnectRecord is processed and sent to all configured URLs concurrently using asynchronous HTTP requests.

+ */ +@Slf4j +@Getter +public class CommonMcpSinkHandler extends AbstractHttpSinkHandler { + + private WebClient webClient; + + + public CommonMcpSinkHandler(SinkConnectorConfig sinkConnectorConfig) { + super(sinkConnectorConfig); + } + + /** + * Initializes the WebClient for making HTTP requests based on the provided SinkConnectorConfig. + */ + @Override + public void start() { + // Create WebClient + doInitWebClient(); + } + + /** + * Initializes the WebClient with the provided configuration options. + */ + private void doInitWebClient() { + SinkConnectorConfig sinkConnectorConfig = getSinkConnectorConfig(); + final Vertx vertx = Vertx.vertx(); + WebClientOptions options = new WebClientOptions() + .setKeepAlive(sinkConnectorConfig.isKeepAlive()) + .setKeepAliveTimeout(sinkConnectorConfig.getKeepAliveTimeout() / 1000) + .setIdleTimeout(sinkConnectorConfig.getIdleTimeout()) + .setIdleTimeoutUnit(TimeUnit.MILLISECONDS) + .setConnectTimeout(sinkConnectorConfig.getConnectionTimeout()) + .setMaxPoolSize(sinkConnectorConfig.getMaxConnectionPoolSize()) + .setPipelining(sinkConnectorConfig.isParallelized()); + this.webClient = WebClient.create(vertx, options); + } + + /** + * Processes HttpConnectRecord on specified URL while returning its own processing logic. This method sends the HttpConnectRecord to the specified + * URL using the WebClient. + * + * @param url URI to which the HttpConnectRecord should be sent + * @param httpConnectRecord HttpConnectRecord to process + * @param attributes additional attributes to be used in processing + * @return processing chain + */ + @Override + public Future> deliver(URI url, HttpConnectRecord httpConnectRecord, Map attributes, + ConnectRecord connectRecord) { + // create headers + Map extensionMap = new HashMap<>(); + Set extensionKeySet = httpConnectRecord.getExtensions().keySet(); + for (String extensionKey : extensionKeySet) { + Object v = httpConnectRecord.getExtensions().getObject(extensionKey); + extensionMap.put(extensionKey, v); + } + + MultiMap headers = HttpHeaders.headers() + .set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8") + .set(HttpHeaderNames.ACCEPT, "application/json; charset=utf-8") + .set("extension", JsonUtils.toJSONString(extensionMap)); + // get timestamp and offset + Long timestamp = httpConnectRecord.getCreateTime() + .atZone(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli(); + + // send the request + return this.webClient.post(url.getPath()) + .host(url.getHost()) + .port(url.getPort() == -1 ? (Objects.equals(url.getScheme(), "https") ? 443 : 80) : url.getPort()) + .putHeaders(headers) + .ssl(Objects.equals(url.getScheme(), "https")) + .sendJson(httpConnectRecord.getData()) + .onSuccess(res -> { + log.info("Request sent successfully. Record: timestamp={}", timestamp); + + Exception e = null; + + // log the response + if (HttpUtils.is2xxSuccessful(res.statusCode())) { + if (log.isDebugEnabled()) { + log.debug("Received successful response: statusCode={}. Record: timestamp={}, responseBody={}", + res.statusCode(), timestamp, res.bodyAsString()); + } else { + log.info("Received successful response: statusCode={}. Record: timestamp={}", res.statusCode(), timestamp); + } + } else { + if (log.isDebugEnabled()) { + log.warn("Received non-2xx response: statusCode={}. Record: timestamp={}, responseBody={}", + res.statusCode(), timestamp, res.bodyAsString()); + } else { + log.warn("Received non-2xx response: statusCode={}. Record: timestamp={}", res.statusCode(), timestamp); + } + + e = new RuntimeException("Unexpected HTTP response code: " + res.statusCode()); + } + + // try callback + tryCallback(httpConnectRecord, e, attributes, connectRecord); + }).onFailure(err -> { + log.error("Request failed to send. Record: timestamp={}", timestamp, err); + + // try callback + tryCallback(httpConnectRecord, err, attributes, connectRecord); + }); + } + + /** + * Tries to call the callback based on the result of the request. + * + * @param httpConnectRecord the HttpConnectRecord to use + * @param e the exception thrown during the request, may be null + * @param attributes additional attributes to be used in processing + */ + private void tryCallback(HttpConnectRecord httpConnectRecord, Throwable e, Map attributes, ConnectRecord record) { + // get and update the attempt event + HttpAttemptEvent attemptEvent = (HttpAttemptEvent) attributes.get(HttpAttemptEvent.PREFIX + httpConnectRecord.getHttpRecordId()); + attemptEvent.updateEvent(e); + + // get and update the multiHttpRequestContext + MultiHttpRequestContext multiHttpRequestContext = getAndUpdateMultiHttpRequestContext(attributes, attemptEvent); + + if (multiHttpRequestContext.isAllRequestsProcessed()) { + // do callback + if (record.getCallback() == null) { + if (log.isDebugEnabled()) { + log.warn("ConnectRecord callback is null. Ignoring callback. {}", record); + } else { + log.warn("ConnectRecord callback is null. Ignoring callback."); + } + return; + } + + // get the last failed event + HttpAttemptEvent lastFailedEvent = multiHttpRequestContext.getLastFailedEvent(); + if (lastFailedEvent == null) { + // success + record.getCallback().onSuccess(convertToSendResult(record)); + } else { + // failure + record.getCallback().onException(buildSendExceptionContext(record, lastFailedEvent.getLastException())); + } + } else { + log.warn("still have requests to process, size {}|attempt num {}", + multiHttpRequestContext.getRemainingRequests(), attemptEvent.getAttempts()); + } + } + + + /** + * Gets and updates the multi http request context based on the provided attributes and HttpConnectRecord. + * + * @param attributes the attributes to use + * @param attemptEvent the HttpAttemptEvent to use + * @return the updated multi http request context + */ + private MultiHttpRequestContext getAndUpdateMultiHttpRequestContext(Map attributes, HttpAttemptEvent attemptEvent) { + // get the multi http request context + MultiHttpRequestContext multiHttpRequestContext = (MultiHttpRequestContext) attributes.get(MultiHttpRequestContext.NAME); + + // Check if the current attempted event has completed + if (attemptEvent.isComplete()) { + // decrement the counter + multiHttpRequestContext.decrementRemainingRequests(); + + if (attemptEvent.getLastException() != null) { + // if all attempts are exhausted, set the last failed event + multiHttpRequestContext.setLastFailedEvent(attemptEvent); + } + } + + return multiHttpRequestContext; + } + + private SendResult convertToSendResult(ConnectRecord record) { + SendResult result = new SendResult(); + result.setMessageId(record.getRecordId()); + if (org.apache.commons.lang3.StringUtils.isNotEmpty(record.getExtension("topic"))) { + result.setTopic(record.getExtension("topic")); + } + return result; + } + + private SendExceptionContext buildSendExceptionContext(ConnectRecord record, Throwable e) { + SendExceptionContext sendExceptionContext = new SendExceptionContext(); + sendExceptionContext.setMessageId(record.getRecordId()); + sendExceptionContext.setCause(e); + if (org.apache.commons.lang3.StringUtils.isNotEmpty(record.getExtension("topic"))) { + sendExceptionContext.setTopic(record.getExtension("topic")); + } + return sendExceptionContext; + } + + + /** + * Cleans up and releases resources used by the HTTP/HTTPS handler. + */ + @Override + public void stop() { + if (this.webClient != null) { + this.webClient.close(); + } else { + log.warn("WebClient is null, ignore."); + } + } +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java new file mode 100644 index 0000000000..6e8f43d6cb --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.handler.impl; + +import dev.failsafe.Failsafe; +import dev.failsafe.RetryPolicy; +import io.vertx.core.Future; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.client.HttpResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.eventmesh.common.config.connector.http.HttpRetryConfig; +import org.apache.eventmesh.common.config.connector.http.SinkConnectorConfig; +import org.apache.eventmesh.connector.http.sink.data.HttpConnectRecord; +import org.apache.eventmesh.connector.http.sink.handler.AbstractHttpSinkHandler; +import org.apache.eventmesh.connector.http.sink.handler.HttpSinkHandler; +import org.apache.eventmesh.connector.http.util.HttpUtils; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; + +import java.net.ConnectException; +import java.net.URI; +import java.time.Duration; +import java.util.Map; + + +/** + * HttpSinkHandlerRetryWrapper is a wrapper class for the HttpSinkHandler that provides retry functionality for failed HTTP requests. + */ +@Slf4j +public class McpSinkHandlerRetryWrapper extends AbstractHttpSinkHandler { + + private final HttpRetryConfig httpRetryConfig; + + private final HttpSinkHandler sinkHandler; + + private final RetryPolicy> retryPolicy; + + public McpSinkHandlerRetryWrapper(SinkConnectorConfig sinkConnectorConfig, HttpSinkHandler sinkHandler) { + super(sinkConnectorConfig); + this.sinkHandler = sinkHandler; + this.httpRetryConfig = getSinkConnectorConfig().getRetryConfig(); + this.retryPolicy = buildRetryPolicy(); + } + + private RetryPolicy> buildRetryPolicy() { + return RetryPolicy.>builder() + .handleIf(e -> e instanceof ConnectException) + .handleResultIf(response -> httpRetryConfig.isRetryOnNonSuccess() && !HttpUtils.is2xxSuccessful(response.statusCode())) + .withMaxRetries(httpRetryConfig.getMaxRetries()) + .withDelay(Duration.ofMillis(httpRetryConfig.getInterval())) + .onRetry(event -> { + if (log.isDebugEnabled()) { + log.warn("Failed to deliver message after {} attempts. Retrying in {} ms. Error: {}", + event.getAttemptCount(), httpRetryConfig.getInterval(), event.getLastException()); + } else { + log.warn("Failed to deliver message after {} attempts. Retrying in {} ms.", + event.getAttemptCount(), httpRetryConfig.getInterval()); + } + }).onFailure(event -> { + if (log.isDebugEnabled()) { + log.error("Failed to deliver message after {} attempts. Error: {}", + event.getAttemptCount(), event.getException()); + } else { + log.error("Failed to deliver message after {} attempts.", + event.getAttemptCount()); + } + }).build(); + } + + /** + * Initializes the WebClient for making HTTP requests based on the provided SinkConnectorConfig. + */ + @Override + public void start() { + sinkHandler.start(); + } + + + /** + * Processes HttpConnectRecord on specified URL while returning its own processing logic This method provides the retry power to process the + * HttpConnectRecord + * + * @param url URI to which the HttpConnectRecord should be sent + * @param httpConnectRecord HttpConnectRecord to process + * @param attributes additional attributes to pass to the processing chain + * @return processing chain + */ + @Override + public Future> deliver(URI url, HttpConnectRecord httpConnectRecord, Map attributes, + ConnectRecord connectRecord) { + Failsafe.with(retryPolicy) + .getStageAsync(() -> sinkHandler.deliver(url, httpConnectRecord, attributes, connectRecord).toCompletionStage()); + return null; + } + + + /** + * Cleans up and releases resources used by the HTTP/HTTPS handler. + */ + @Override + public void stop() { + sinkHandler.stop(); + } +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java new file mode 100644 index 0000000000..a0fe32d967 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.source; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.ext.web.Route; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.LoggerHandler; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.eventmesh.common.config.connector.Config; +import org.apache.eventmesh.common.config.connector.http.HttpSourceConfig; +import org.apache.eventmesh.common.exception.EventMeshException; +import org.apache.eventmesh.connector.mcp.source.protocol.Protocol; +import org.apache.eventmesh.connector.mcp.source.protocol.ProtocolFactory; +import org.apache.eventmesh.openconnect.api.ConnectorCreateService; +import org.apache.eventmesh.openconnect.api.connector.ConnectorContext; +import org.apache.eventmesh.openconnect.api.connector.SourceConnectorContext; +import org.apache.eventmesh.openconnect.api.source.Source; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class McpSourceConnector implements Source, ConnectorCreateService { + + private HttpSourceConfig sourceConfig; + + private BlockingQueue queue; + + private int batchSize; + + private Route route; + + private Protocol protocol; + + private HttpServer server; + + @Getter + private volatile boolean started = false; + + @Getter + private volatile boolean destroyed = false; + + + @Override + public Class configClass() { + return HttpSourceConfig.class; + } + + @Override + public Source create() { + return new McpSourceConnector(); + } + + @Override + public void init(Config config) { + this.sourceConfig = (HttpSourceConfig) config; + doInit(); + } + + @Override + public void init(ConnectorContext connectorContext) { + SourceConnectorContext sourceConnectorContext = (SourceConnectorContext) connectorContext; + this.sourceConfig = (HttpSourceConfig) sourceConnectorContext.getSourceConfig(); + doInit(); + } + + private void doInit() { + // init queue + int maxQueueSize = this.sourceConfig.getConnectorConfig().getMaxStorageSize(); + this.queue = new LinkedBlockingQueue<>(maxQueueSize); + + // init batch size + this.batchSize = this.sourceConfig.getConnectorConfig().getBatchSize(); + + // init protocol + String protocolName = this.sourceConfig.getConnectorConfig().getProtocol(); + this.protocol = ProtocolFactory.getInstance(this.sourceConfig.connectorConfig, protocolName); + + final Vertx vertx = Vertx.vertx(); + final Router router = Router.router(vertx); + route = router.route() + .path(this.sourceConfig.connectorConfig.getPath()) + .handler(LoggerHandler.create()); + + // set protocol handler + this.protocol.setHandler(route, queue); + + // create server + this.server = vertx.createHttpServer(new HttpServerOptions() + .setPort(this.sourceConfig.connectorConfig.getPort()) + .setMaxFormAttributeSize(this.sourceConfig.connectorConfig.getMaxFormAttributeSize()) + .setIdleTimeout(this.sourceConfig.connectorConfig.getIdleTimeout()) + .setIdleTimeoutUnit(TimeUnit.MILLISECONDS)).requestHandler(router); + } + + @Override + public void start() { + this.server.listen(res -> { + if (res.succeeded()) { + this.started = true; + log.info("HttpSourceConnector started on port: {}", this.sourceConfig.getConnectorConfig().getPort()); + } else { + log.error("HttpSourceConnector failed to start on port: {}", this.sourceConfig.getConnectorConfig().getPort()); + throw new EventMeshException("failed to start Vertx server", res.cause()); + } + }); + } + + @Override + public void commit(ConnectRecord record) { + if (sourceConfig.getConnectorConfig().isDataConsistencyEnabled()) { + log.debug("HttpSourceConnector commit record: {}", record.getRecordId()); + RoutingContext routingContext = (RoutingContext) record.getExtensionObj("routingContext"); + if (routingContext != null) { + routingContext.response() + .putHeader("content-type", "application/json") + .setStatusCode(HttpResponseStatus.OK.code()) + .end(CommonResponse.success().toJsonStr()); + } else { + log.error("Failed to commit the record, routingContext is null, recordId: {}", record.getRecordId()); + } + } + } + + @Override + public String name() { + return this.sourceConfig.getConnectorConfig().getConnectorName(); + } + + @Override + public void onException(ConnectRecord record) { + if (this.route != null) { + this.route.failureHandler(ctx -> { + log.error("Failed to handle the request, recordId {}. ", record.getRecordId(), ctx.failure()); + // Return Bad Response + ctx.response() + .setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()) + .end("{\"status\":\"failed\",\"recordId\":\"" + record.getRecordId() + "\"}"); + }); + } + } + + @Override + public void stop() { + if (this.server != null) { + this.server.close(res -> { + if (res.succeeded()) { + this.destroyed = true; + log.info("HttpSourceConnector stopped on port: {}", this.sourceConfig.getConnectorConfig().getPort()); + } else { + log.error("HttpSourceConnector failed to stop on port: {}", this.sourceConfig.getConnectorConfig().getPort()); + throw new EventMeshException("failed to stop Vertx server", res.cause()); + } + } + ); + } else { + log.warn("HttpSourceConnector server is null, ignore."); + } + } + + @Override + public List poll() { + long startTime = System.currentTimeMillis(); + long maxPollWaitTime = 5000; + long remainingTime = maxPollWaitTime; + + // poll from queue + List connectRecords = new ArrayList<>(batchSize); + for (int i = 0; i < batchSize; i++) { + try { + Object obj = queue.poll(remainingTime, TimeUnit.MILLISECONDS); + if (obj == null) { + break; + } + // convert to ConnectRecord + ConnectRecord connectRecord = protocol.convertToConnectRecord(obj); + connectRecords.add(connectRecord); + + // calculate elapsed time and update remaining time for next poll + long elapsedTime = System.currentTimeMillis() - startTime; + remainingTime = maxPollWaitTime > elapsedTime ? maxPollWaitTime - elapsedTime : 0; + } catch (Exception e) { + log.error("Failed to poll from queue.", e); + throw new RuntimeException(e); + } + + } + return connectRecords; + } + +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java new file mode 100644 index 0000000000..fafb02548a --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java @@ -0,0 +1,32 @@ +package org.apache.eventmesh.connector.mcp.source.data; + +import io.vertx.ext.web.RoutingContext; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * Mcp Protocol Request. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class McpRequest implements Serializable { + private static final long serialVersionUID = -483500600756490500L; + + private String protocolName; + + private String sessionId; + + private Map metadata; + + private Boolean stream; + + private List inputs; + + private RoutingContext routingContext; +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java new file mode 100644 index 0000000000..ff8717e600 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java @@ -0,0 +1,44 @@ +package org.apache.eventmesh.connector.mcp.source.data; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONWriter.Feature; +import java.util.Map; + +/** + * Webhook response. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class McpResponse implements Serializable { + private static final long serialVersionUID = 8616938575207104455L; + + private Object outputs; + + private String sessionId; + + private Map metadata; + + private String finishReason; + + private LocalDateTime handleTime; + + public String toJsonStr() { + return JSON.toJSONString(this, Feature.WriteMapNullValue); + } + + public static McpResponse success(Object outputs, String sessionId) { + return new McpResponse(outputs, sessionId, null, "done", LocalDateTime.now()); + } + + + public static McpResponse base(Object outputs, String sessionId, String finishReason) { + return new McpResponse(outputs, sessionId, null, finishReason, LocalDateTime.now()); + } +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/McpConstants.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/McpConstants.java similarity index 53% rename from eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/McpConstants.java rename to eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/McpConstants.java index 104014c864..1d7ece2979 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/protocol/McpConstants.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/McpConstants.java @@ -1,4 +1,4 @@ -package org.apache.eventmesh.connector.mcp.protocol; +package org.apache.eventmesh.connector.mcp.source.protocol; // header key、content-type 常量等 public class McpConstants { diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java new file mode 100644 index 0000000000..d0ac2d06a8 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.source.protocol; + +import io.vertx.ext.web.Route; +import org.apache.eventmesh.common.config.connector.http.SourceConnectorConfig; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; + +import java.util.concurrent.BlockingQueue; + + +/** + * Protocol Interface. + * All protocols should implement this interface. + */ +public interface Protocol { + + + /** + * Initialize the protocol. + * + * @param sourceConnectorConfig source connector config + */ + void initialize(SourceConnectorConfig sourceConnectorConfig); + + + /** + * Handle the protocol message. + * + * @param route route + * @param queue queue info + */ + void setHandler(Route route, BlockingQueue queue); + + + /** + * Convert the message to ConnectRecord. + * + * @param message message + * @return ConnectRecord + */ + ConnectRecord convertToConnectRecord(Object message); +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/ProtocolFactory.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/ProtocolFactory.java new file mode 100644 index 0000000000..15e4ca3e77 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/ProtocolFactory.java @@ -0,0 +1,58 @@ +package org.apache.eventmesh.connector.mcp.source.protocol; + +import org.apache.eventmesh.common.config.connector.http.SourceConnectorConfig; +import org.apache.eventmesh.connector.mcp.source.protocol.impl.McpStandardProtocol; + +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * Protocol factory. This class is responsible for storing and creating instances of {@link Protocol} classes. + */ +public class ProtocolFactory { + // protocol name -> protocol class + private static final ConcurrentHashMap> protocols = new ConcurrentHashMap<>(); + + static { + // register all protocols + registerProtocol(McpStandardProtocol.PROTOCOL_NAME, McpStandardProtocol.class); + } + + + /** + * Register a protocol + * + * @param name name of the protocol + * @param clazz class of the protocol + */ + public static void registerProtocol(String name, Class clazz) { + if (Protocol.class.isAssignableFrom(clazz)) { + // put the class into the map(case insensitive) + protocols.put(name.toLowerCase(), clazz); + } else { + throw new IllegalArgumentException("Class " + clazz.getName() + " does not implement Protocol interface"); + } + } + + /** + * Get an instance of a protocol, if it is not already created, create a new instance + * + * @param name name of the protocol + * @return instance of the protocol + */ + public static Protocol getInstance(SourceConnectorConfig sourceConnectorConfig, String name) { + // get the class by name(case insensitive) + Class clazz = Optional.ofNullable(protocols.get(name.toLowerCase())) + .orElseThrow(() -> new IllegalArgumentException("Protocol " + name + " is not registered")); + try { + // create a new instance + Protocol protocol = (Protocol) clazz.newInstance(); + // initialize the protocol + protocol.initialize(sourceConnectorConfig); + return protocol; + } catch (InstantiationException | IllegalAccessException e) { + throw new IllegalArgumentException("Failed to instantiate protocol " + name, e); + } + } +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java new file mode 100644 index 0000000000..d495b06b5d --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java @@ -0,0 +1,119 @@ +package org.apache.eventmesh.connector.mcp.source.protocol.impl; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Route; +import io.vertx.ext.web.handler.BodyHandler; +import lombok.extern.slf4j.Slf4j; +import org.apache.eventmesh.common.Constants; +import org.apache.eventmesh.common.config.connector.http.SourceConnectorConfig; +import org.apache.eventmesh.common.utils.JsonUtils; +import org.apache.eventmesh.connector.mcp.source.data.McpRequest; +import org.apache.eventmesh.connector.mcp.source.protocol.Protocol; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; + +import java.util.Base64; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.stream.Collectors; + +// 解析标准 2025-03-26 spec 请求 +/** + * Mcp Protocol. This class represents the mcp protocol. The processing method of this class does not perform any other operations + * except storing the request and returning a general response. + */ +@Slf4j +public class McpStandardProtocol implements Protocol { + public static final String PROTOCOL_NAME = "Common"; + + private SourceConnectorConfig sourceConnectorConfig; + + /** + * Initialize the protocol + * + * @param sourceConnectorConfig source connector config + */ + @Override + public void initialize(SourceConnectorConfig sourceConnectorConfig) { + this.sourceConnectorConfig = sourceConnectorConfig; + } + + /** + * Set the handler for the route + * + * @param route route + * @param queue queue info + */ + @Override + public void setHandler(Route route, BlockingQueue queue) { + route.method(HttpMethod.POST) + .handler(BodyHandler.create()) + .handler(ctx -> { + // Get the payload + Object payload = ctx.body().asString(Constants.DEFAULT_CHARSET.toString()); + payload = JsonUtils.parseObject(payload.toString(), String.class); + + // Create and store the webhook request + Map headerMap = ctx.request().headers().entries().stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + WebhookRequest webhookRequest = new WebhookRequest(PROTOCOL_NAME, ctx.request().absoluteURI(), headerMap, payload, ctx); + if (!queue.offer(webhookRequest)) { + throw new IllegalStateException("Failed to store the request."); + } + + if (!sourceConnectorConfig.isDataConsistencyEnabled()) { + // Return 200 OK + ctx.response() + .setStatusCode(HttpResponseStatus.OK.code()) + .end(CommonResponse.success().toJsonStr()); + } + + }) + .failureHandler(ctx -> { + log.error("Failed to handle the request. ", ctx.failure()); + + // Return Bad Response + ctx.response() + .setStatusCode(ctx.statusCode()) + .end(CommonResponse.base(ctx.failure().getMessage()).toJsonStr()); + }); + + } + + /** + * Convert the message to a connect record + * + * @param message message + * @return connect record + */ + @Override + public ConnectRecord convertToConnectRecord(Object message) { + McpRequest request = (McpRequest) message; + ConnectRecord connectRecord = new ConnectRecord(null, null, System.currentTimeMillis(), request.getPayload()); + connectRecord.addExtension("source", request.getProtocolName()); + connectRecord.addExtension("url", request.getUrl()); + request.getHeaders().forEach((k, v) -> { + if (k.equalsIgnoreCase("extension")) { + JsonObject extension = new JsonObject(v); + extension.forEach(e -> connectRecord.addExtension(e.getKey(), e.getValue())); + } + }); + // check recordUniqueId + if (!connectRecord.getExtensions().containsKey("recordUniqueId")) { + connectRecord.addExtension("recordUniqueId", connectRecord.getRecordId()); + } + + // check data + if (connectRecord.getExtensionObj("isBase64") != null) { + if (Boolean.parseBoolean(connectRecord.getExtensionObj("isBase64").toString())) { + byte[] data = Base64.getDecoder().decode(connectRecord.getData().toString()); + connectRecord.setData(data); + } + } + if (request.getRoutingContext() != null) { + connectRecord.addExtension("routingContext", request.getRoutingContext()); + } + return connectRecord; + } +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/source-config.yml b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/source-config.yml new file mode 100644 index 0000000000..937b58f94e --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/source-config.yml @@ -0,0 +1,42 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +pubSubConfig: + meshAddress: 127.0.0.1:10000 + subject: TopicTest + idc: FT + env: PRD + group: httpSource + appId: 5032 + userName: httpSourceUser + passWord: httpPassWord +connectorConfig: + connectorName: mcpSource + path: /mcp/message + port: 7091 + idleTimeout: 5000 # timeunit: ms + maxFormAttributeSize: 1048576 # timeunit: byte, default: 1048576(1MB). This applies only when handling form data submissions. + protocol: MCP # Case insensitive, default: CloudEvent, options: CloudEvent, GitHub, Common + extraConfig: # extra config for different protocol, e.g. GitHub secret + streamType: chunked + contentType: application/json + reconnection: true +sessionConfig: + sessionStorage: redis + redisHost: 127.0.0.1 + redisPort: 6379 + sessionTimeout: 10m \ No newline at end of file diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/build.gradle b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/build.gradle new file mode 100644 index 0000000000..d219c5dc03 --- /dev/null +++ b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/build.gradle @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +dependencies { + implementation project(":eventmesh-protocol-plugin:eventmesh-protocol-api") + implementation "io.cloudevents:cloudevents-core" + implementation "com.google.guava:guava" + implementation "io.cloudevents:cloudevents-json-jackson" + implementation ("io.grpc:grpc-protobuf:1.68.0") { + exclude group: "com.google.protobuf", module: "protobuf-java" + } + implementation("com.google.protobuf:protobuf-java:3.25.4") +} diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/gradle.porperties b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/gradle.porperties new file mode 100644 index 0000000000..83a61e1720 --- /dev/null +++ b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/gradle.porperties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +pluginType=protocol +pluginName=mcp \ No newline at end of file diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolAdaptor.java b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolAdaptor.java new file mode 100644 index 0000000000..7dbd33fee2 --- /dev/null +++ b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolAdaptor.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.protocol.mcp; + +import com.fasterxml.jackson.core.type.TypeReference; +import io.cloudevents.CloudEvent; +import org.apache.eventmesh.common.Constants; +import org.apache.eventmesh.common.protocol.ProtocolTransportObject; +import org.apache.eventmesh.common.protocol.http.HttpEventWrapper; +import org.apache.eventmesh.common.protocol.http.common.RequestURI; +import org.apache.eventmesh.common.utils.JsonUtils; +import org.apache.eventmesh.protocol.api.ProtocolAdaptor; +import org.apache.eventmesh.protocol.api.exception.ProtocolHandleException; +import org.apache.eventmesh.protocol.http.resolver.HttpRequestProtocolResolver; + +import java.nio.charset.StandardCharsets; +import java.util.*; + +import static org.apache.eventmesh.protocol.http.HttpProtocolConstant.*; + +/** + * CloudEvents protocol adaptor, used to transform CloudEvents message to CloudEvents message. + * + * @since 1.3.0 + */ +public class McpProtocolAdaptor + implements ProtocolAdaptor { + + @Override + public CloudEvent toCloudEvent(ProtocolTransportObject protocolTransportObject) throws ProtocolHandleException { + + if (protocolTransportObject instanceof HttpEventWrapper) { + HttpEventWrapper httpEventWrapper = (HttpEventWrapper) protocolTransportObject; + String requestURI = httpEventWrapper.getRequestURI(); + + return deserializeProtocol(requestURI, httpEventWrapper); + + } else { + throw new ProtocolHandleException(String.format("protocol class: %s", protocolTransportObject.getClass())); + } + } + + private CloudEvent deserializeProtocol(String requestURI, HttpEventWrapper httpEventWrapper) throws ProtocolHandleException { + + if (requestURI.startsWith(RequestURI.PUBLISH.getRequestURI()) || requestURI.startsWith(RequestURI.PUBLISH_BRIDGE.getRequestURI())) { + return HttpRequestProtocolResolver.buildEvent(httpEventWrapper); + } else { + throw new ProtocolHandleException(String.format("unsupported requestURI: %s", requestURI)); + } + + } + + @Override + public List toBatchCloudEvent(ProtocolTransportObject protocol) + throws ProtocolHandleException { + return Collections.emptyList(); + } + + @Override + public ProtocolTransportObject fromCloudEvent(CloudEvent cloudEvent) throws ProtocolHandleException { + HttpEventWrapper httpEventWrapper = new HttpEventWrapper(); + Map sysHeaderMap = new HashMap<>(); + // ce attributes + Set attributeNames = cloudEvent.getAttributeNames(); + // ce extensions + Set extensionNames = cloudEvent.getExtensionNames(); + for (String attributeName : attributeNames) { + sysHeaderMap.put(attributeName, cloudEvent.getAttribute(attributeName)); + } + for (String extensionName : extensionNames) { + sysHeaderMap.put(extensionName, cloudEvent.getExtension(extensionName)); + } + httpEventWrapper.setSysHeaderMap(sysHeaderMap); + // ce data + if (cloudEvent.getData() != null) { + Map dataContentMap = JsonUtils.parseTypeReferenceObject( + new String(Objects.requireNonNull(cloudEvent.getData()).toBytes(), Constants.DEFAULT_CHARSET), + new TypeReference>() { + }); + String requestHeader = JsonUtils.toJSONString( + Objects.requireNonNull(dataContentMap, "Headers must not be null").get(CONSTANTS_KEY_HEADERS)); + byte[] requestBody = Objects.requireNonNull( + JsonUtils.toJSONString(dataContentMap.get(CONSTANTS_KEY_BODY)), "Body must not be null").getBytes(StandardCharsets.UTF_8); + Map requestHeaderMap = JsonUtils.parseTypeReferenceObject(requestHeader, new TypeReference>() { + }); + String requestURI = dataContentMap.get(CONSTANTS_KEY_PATH).toString(); + String httpMethod = dataContentMap.get(CONSTANTS_KEY_METHOD).toString(); + + httpEventWrapper.setHeaderMap(requestHeaderMap); + httpEventWrapper.setBody(requestBody); + httpEventWrapper.setRequestURI(requestURI); + httpEventWrapper.setHttpMethod(httpMethod); + } + return httpEventWrapper; + + } + + @Override + public String getProtocolType() { + return McpProtocolConstant.PROTOCOL_NAME; + } +} diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolConstant.java b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolConstant.java new file mode 100644 index 0000000000..1c42cc190b --- /dev/null +++ b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolConstant.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.protocol.mcp; + +public enum McpProtocolConstant { + ; + + public static final String PROTOCOL_NAME = "http"; + + public static final String CONSTANTS_DEFAULT_SOURCE = "/"; + public static final String CONSTANTS_DEFAULT_TYPE = "http_request"; + public static final String CONSTANTS_DEFAULT_SUBJECT = ""; + + public static final String CONSTANTS_KEY_ID = "id"; + public static final String CONSTANTS_KEY_SOURCE = "source"; + public static final String CONSTANTS_KEY_TYPE = "type"; + public static final String CONSTANTS_KEY_SUBJECT = "subject"; + public static final String CONSTANTS_KEY_HEADERS = "headers"; + public static final String CONSTANTS_KEY_BODY = "body"; + public static final String CONSTANTS_KEY_PATH = "path"; + public static final String CONSTANTS_KEY_METHOD = "method"; + + public static final String DATA_CONTENT_TYPE = "Content-Type"; + + public static final String APPLICATION_JSON = "application/json"; + public static final String PROTOBUF = "application/x-protobuf"; +} diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/resolver/McpRequestProtocolResolver.java b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/resolver/McpRequestProtocolResolver.java new file mode 100644 index 0000000000..d26b977cdf --- /dev/null +++ b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/resolver/McpRequestProtocolResolver.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.protocol.mcp.resolver; + +import com.fasterxml.jackson.core.type.TypeReference; +import io.cloudevents.CloudEvent; +import io.cloudevents.core.v1.CloudEventBuilder; +import org.apache.commons.lang3.StringUtils; +import org.apache.eventmesh.common.protocol.http.HttpEventWrapper; +import org.apache.eventmesh.common.utils.JsonUtils; +import org.apache.eventmesh.protocol.api.exception.ProtocolHandleException; +import org.apache.eventmesh.protocol.http.HttpProtocolConstant; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; + +public class McpRequestProtocolResolver { + + public static CloudEvent buildEvent(HttpEventWrapper httpEventWrapper) throws ProtocolHandleException { + + try { + CloudEventBuilder builder = new CloudEventBuilder(); + + Map requestHeaderMap = httpEventWrapper.getHeaderMap(); + + Map sysHeaderMap = httpEventWrapper.getSysHeaderMap(); + + String id = sysHeaderMap.getOrDefault(HttpProtocolConstant.CONSTANTS_KEY_ID, UUID.randomUUID()).toString(); + + String source = sysHeaderMap.getOrDefault(HttpProtocolConstant.CONSTANTS_KEY_SOURCE, + HttpProtocolConstant.CONSTANTS_DEFAULT_SOURCE).toString(); + + String type = sysHeaderMap.getOrDefault(HttpProtocolConstant.CONSTANTS_KEY_TYPE, + HttpProtocolConstant.CONSTANTS_DEFAULT_TYPE).toString(); + + String subject = sysHeaderMap.getOrDefault(HttpProtocolConstant.CONSTANTS_KEY_SUBJECT, + HttpProtocolConstant.CONSTANTS_DEFAULT_SUBJECT).toString(); + + String dataContentType = requestHeaderMap.getOrDefault(HttpProtocolConstant.DATA_CONTENT_TYPE, + HttpProtocolConstant.APPLICATION_JSON).toString(); + // with attributes + builder.withId(id) + .withType(type) + .withSource(URI.create(HttpProtocolConstant.CONSTANTS_KEY_SOURCE + ":" + source)) + .withSubject(subject) + .withDataContentType(dataContentType); + + // with extensions + for (Map.Entry extension : sysHeaderMap.entrySet()) { + if (StringUtils.equals(HttpProtocolConstant.CONSTANTS_KEY_ID, extension.getKey()) + || StringUtils.equals(HttpProtocolConstant.CONSTANTS_KEY_SOURCE, extension.getKey()) + || StringUtils.equals(HttpProtocolConstant.CONSTANTS_KEY_TYPE, extension.getKey()) + || StringUtils.equals(HttpProtocolConstant.CONSTANTS_KEY_SUBJECT, extension.getKey())) { + continue; + } + String lowerExtensionKey = extension.getKey().toLowerCase(Locale.getDefault()); + builder.withExtension(lowerExtensionKey, sysHeaderMap.get(extension.getKey()).toString()); + } + + byte[] requestBody = httpEventWrapper.getBody(); + + if (StringUtils.equals(dataContentType, HttpProtocolConstant.APPLICATION_JSON)) { + Map requestBodyMap = JsonUtils.parseTypeReferenceObject(new String(requestBody), + new TypeReference>() { + }); + + String requestURI = httpEventWrapper.getRequestURI(); + + Map data = new HashMap<>(); + data.put(HttpProtocolConstant.CONSTANTS_KEY_HEADERS, requestHeaderMap); + data.put(HttpProtocolConstant.CONSTANTS_KEY_BODY, requestBodyMap); + data.put(HttpProtocolConstant.CONSTANTS_KEY_PATH, requestURI); + data.put(HttpProtocolConstant.CONSTANTS_KEY_METHOD, httpEventWrapper.getHttpMethod()); + // with data + builder = builder.withData(JsonUtils.toJSONString(data).getBytes(StandardCharsets.UTF_8)); + } else if (StringUtils.equals(dataContentType, HttpProtocolConstant.PROTOBUF)) { + // with data + builder = builder.withData(requestBody); + } + return builder.build(); + } catch (Exception e) { + throw new ProtocolHandleException(e.getMessage(), e); + } + } +} diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/session/McpSession.java b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/session/McpSession.java new file mode 100644 index 0000000000..b31c7dae79 --- /dev/null +++ b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/session/McpSession.java @@ -0,0 +1,4 @@ +package org.apache.eventmesh.protocol.mcp.session; + +public class McpSession { +} diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/session/McpSessionManager.java b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/session/McpSessionManager.java new file mode 100644 index 0000000000..6e7fd810c0 --- /dev/null +++ b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/session/McpSessionManager.java @@ -0,0 +1,4 @@ +package org.apache.eventmesh.protocol.mcp.session; + +public class McpSessionManager { +} diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/resources/eventmesh/org.apache.eventmesh.protocol.mcp.ProtocolAdaptor b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/resources/eventmesh/org.apache.eventmesh.protocol.mcp.ProtocolAdaptor new file mode 100644 index 0000000000..b086ff7083 --- /dev/null +++ b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/resources/eventmesh/org.apache.eventmesh.protocol.mcp.ProtocolAdaptor @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +mcp=org.apache.eventmesh.protocol.mcp.McpProtocolAdaptor \ No newline at end of file diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshStartup.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshStartup.java index a357ec5eee..5c6ae90aae 100644 --- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshStartup.java +++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshStartup.java @@ -31,8 +31,7 @@ public class EventMeshStartup { public static void main(String[] args) throws Exception { try { ConfigService.getInstance() - //.setConfigPath(EventMeshConstants.EVENTMESH_CONF_HOME + File.separator) - .setConfigPath("eventmesh-runtime/conf") + .setConfigPath(EventMeshConstants.EVENTMESH_CONF_HOME + File.separator) .setRootConfig(EventMeshConstants.EVENTMESH_CONF_FILE); EventMeshServer server = new EventMeshServer(); From 3d216a912002603a90d796c1c2ffcb8b9352ce76 Mon Sep 17 00:00:00 2001 From: jerryyummy <461697582@qq.com> Date: Wed, 2 Jul 2025 11:16:09 +0800 Subject: [PATCH 03/36] basic arch --- .../config/connector/mcp/HttpMcpConfig.java | 48 ++++++++ .../config/connector/mcp/HttpSinkConfig.java | 30 +++++ .../connector/mcp/HttpSourceConfig.java | 30 +++++ .../connector/mcp/SinkConnectorConfig.java | 83 ++++++++++++++ .../connector/mcp/SourceConnectorConfig.java | 61 ++++++++++ .../common/protocol/mcp/McpEventWrapper.java | 95 ++++++++++++++++ .../eventmesh-connector-mcp/build.gradle | 45 +++++--- .../mcp/server/McpConnectServer.java | 6 + .../connector/mcp/session/McpSession.java | 59 +++++++++- .../mcp/session/McpSessionManager.java | 55 ++++++++- .../connector/mcp/sink/McpSinkConnector.java | 9 +- .../mcp/sink/data/McpExportRecordPage.java | 3 +- .../mcp/sink/data/MultiMcpRequestContext.java | 14 +-- .../protocol/mcp/McpProtocolAdaptor.java | 107 ++++++------------ .../protocol/mcp/McpProtocolConstant.java | 25 ++-- .../resolver/McpRequestProtocolResolver.java | 96 ++++++++-------- .../protocol/mcp/session/McpSession.java | 4 - .../mcp/session/McpSessionManager.java | 4 - settings.gradle | 1 + 19 files changed, 595 insertions(+), 180 deletions(-) create mode 100644 eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpMcpConfig.java create mode 100644 eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpSinkConfig.java create mode 100644 eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpSourceConfig.java create mode 100644 eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SinkConnectorConfig.java create mode 100644 eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SourceConnectorConfig.java create mode 100644 eventmesh-common/src/main/java/org/apache/eventmesh/common/protocol/mcp/McpEventWrapper.java delete mode 100644 eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/session/McpSession.java delete mode 100644 eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/session/McpSessionManager.java diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpMcpConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpMcpConfig.java new file mode 100644 index 0000000000..5fc2f0b9d9 --- /dev/null +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpMcpConfig.java @@ -0,0 +1,48 @@ +package org.apache.eventmesh.common.config.connector.mcp; + +import lombok.Data; + +/** + * MCP Server 接入配置 + */ +@Data +public class HttpMcpConfig { + + /** + * 是否启用 MCP 服务 + */ + private boolean activate = false; + + /** + * MCP Server 监听端口 + */ + private int port; + + // Path to display/export callback data + private String exportPath = "/export"; + + /** + * Session 空闲超时时间(单位:毫秒) + */ + private int sessionIdleTimeoutMs = 300_000; + + /** + * 最大 Session 数(用于流式连接控制) + */ + private int maxConcurrentSessions = 500; + + /** + * 是否启用流式响应(Chunked Response) + */ + private boolean enableStreaming = true; + + /** + * 每个会话最大缓存消息数 + */ + private int maxBufferedMessagesPerSession = 100; + + /** + * 启动时是否打印 MCP 请求日志 + */ + private boolean enableDebugLog = false; +} diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpSinkConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpSinkConfig.java new file mode 100644 index 0000000000..866c228a88 --- /dev/null +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpSinkConfig.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.common.config.connector.mcp; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.eventmesh.common.config.connector.SinkConfig; +import org.apache.eventmesh.common.config.connector.http.SinkConnectorConfig; + +@Data +@EqualsAndHashCode(callSuper = true) +public class HttpSinkConfig extends SinkConfig { + + public SinkConnectorConfig connectorConfig; +} diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpSourceConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpSourceConfig.java new file mode 100644 index 0000000000..50ee8a2440 --- /dev/null +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpSourceConfig.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.common.config.connector.mcp; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.eventmesh.common.config.connector.SourceConfig; +import org.apache.eventmesh.common.config.connector.http.SourceConnectorConfig; + +@Data +@EqualsAndHashCode(callSuper = true) +public class HttpSourceConfig extends SourceConfig { + + public SourceConnectorConfig connectorConfig; +} diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SinkConnectorConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SinkConnectorConfig.java new file mode 100644 index 0000000000..3cb56d9362 --- /dev/null +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SinkConnectorConfig.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.common.config.connector.mcp; + + +import lombok.Data; +import org.apache.eventmesh.common.config.connector.http.HttpRetryConfig; +import org.apache.eventmesh.common.config.connector.http.HttpWebhookConfig; + +@Data +public class SinkConnectorConfig { + + private String connectorName; + + private String[] urls; + + // keepAlive, default true + private boolean keepAlive = true; + + // timeunit: ms, default 60000ms + private int keepAliveTimeout = 60 * 1000; // Keep units consistent + + // timeunit: ms, default 5000ms, recommended scope: 5000ms - 10000ms + private int connectionTimeout = 5000; + + // timeunit: ms, default 5000ms + private int idleTimeout = 5000; + + // maximum number of HTTP/1 connections a client will pool, default 50 + private int maxConnectionPoolSize = 50; + + // retry config + private HttpRetryConfig retryConfig = new HttpRetryConfig(); + + // mcp config + private HttpMcpConfig mcpConfig = new HttpMcpConfig(); + + private String deliveryStrategy = "ROUND_ROBIN"; + + private boolean skipDeliverException = false; + + // managed pipelining param, default true + private boolean isParallelized = true; + + private int parallelism = 2; + + + /** + * Fill default values if absent (When there are multiple default values for a field) + * + * @param config SinkConnectorConfig + */ + public static void populateFieldsWithDefaults(SinkConnectorConfig config) { + /* + * set default values for idleTimeout + * recommended scope: common(5s - 10s), mcp(15s - 30s) + */ + final int commonHttpIdleTimeout = 5000; + final int mcpHttpIdleTimeout = 15000; + + // Set default values for idleTimeout + if (config.getIdleTimeout() == 0) { + int idleTimeout = config.mcpConfig.isActivate() ? mcpHttpIdleTimeout : commonHttpIdleTimeout; + config.setIdleTimeout(idleTimeout); + } + + } +} diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SourceConnectorConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SourceConnectorConfig.java new file mode 100644 index 0000000000..98a7025b1a --- /dev/null +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SourceConnectorConfig.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.common.config.connector.mcp; + +import lombok.Data; + +import java.util.HashMap; +import java.util.Map; + +@Data +public class SourceConnectorConfig { + + private String connectorName; + + private String path = "/mcp"; + + private int port; + + // timeunit: ms, default 5000ms + private int idleTimeout = 5000; + + /** + *
    + *
  • The maximum size allowed for form attributes when Content-Type is application/x-www-form-urlencoded or multipart/form-data
  • + *
  • Default is 1MB (1024 * 1024 bytes).
  • + *
  • If you receive a "size exceed allowed maximum capacity" error, you can increase this value.
  • + *
  • Note: This applies only when handling form data submissions.
  • + *
+ */ + private int maxFormAttributeSize = 1024 * 1024; + + // max size of the queue, default 1000 + private int maxStorageSize = 1000; + + // batch size, default 10 + private int batchSize = 10; + + // protocol, default CloudEvent + private String protocol = "Mcp"; + + // extra config, e.g. GitHub secret + private Map extraConfig = new HashMap<>(); + + // data consistency enabled, default true + private boolean dataConsistencyEnabled = true; +} diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/protocol/mcp/McpEventWrapper.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/protocol/mcp/McpEventWrapper.java new file mode 100644 index 0000000000..70b6bdd40b --- /dev/null +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/protocol/mcp/McpEventWrapper.java @@ -0,0 +1,95 @@ +package org.apache.eventmesh.common.protocol.mcp; + +import org.apache.eventmesh.common.protocol.ProtocolTransportObject; +import org.apache.eventmesh.common.protocol.http.common.ProtocolKey; +import org.apache.commons.lang3.StringUtils; + +import java.net.URI; +import java.util.*; + +public class McpEventWrapper implements ProtocolTransportObject { + + private transient Map headerMap = new HashMap<>(); + + private transient Map sysHeaderMap = new HashMap<>(); + + private byte[] body; + + private long reqTime; + + private long resTime; + + public McpEventWrapper() { + this.reqTime = System.currentTimeMillis(); + } + + public Map getHeaderMap() { + return headerMap; + } + + public void setHeaderMap(Map headerMap) { + this.headerMap = headerMap; + } + + public Map getSysHeaderMap() { + return sysHeaderMap; + } + + public void setSysHeaderMap(Map sysHeaderMap) { + this.sysHeaderMap = sysHeaderMap; + } + + public byte[] getBody() { + int len = body.length; + byte[] b = new byte[len]; + System.arraycopy(body, 0, b, 0, len); + return b; + } + + public void setBody(byte[] newBody) { + if (newBody == null || newBody.length == 0) { + return; + } + this.body = Arrays.copyOf(newBody, newBody.length); + } + + public long getReqTime() { + return reqTime; + } + + public void setReqTime(long reqTime) { + this.reqTime = reqTime; + } + + public long getResTime() { + return resTime; + } + + public void setResTime(long resTime) { + this.resTime = resTime; + } + + public void buildSysHeaderForClient() { + sysHeaderMap.put(ProtocolKey.PROTOCOL_TYPE, "mcp"); + sysHeaderMap.put(ProtocolKey.PROTOCOL_DESC, "mcp"); + + for (ProtocolKey.ClientInstanceKey key : ProtocolKey.ClientInstanceKey.values()) { + if (key == ProtocolKey.ClientInstanceKey.BIZSEQNO || key == ProtocolKey.ClientInstanceKey.UNIQUEID) { + continue; + } + sysHeaderMap.put(key.getKey(), headerMap.getOrDefault(key.getKey(), key.getValue())); + } + } + + public void buildSysHeaderForCE() { + sysHeaderMap.put(ProtocolKey.CloudEventsKey.ID, UUID.randomUUID().toString()); + sysHeaderMap.put(ProtocolKey.CloudEventsKey.SOURCE, headerMap.getOrDefault("source", URI.create("/"))); + sysHeaderMap.put(ProtocolKey.CloudEventsKey.TYPE, headerMap.getOrDefault("type", "mcp_request")); + + String topic = headerMap.getOrDefault("subject", "").toString(); + if (StringUtils.isEmpty(topic)) { + topic = "TEST-MCP-TOPIC"; + } + sysHeaderMap.put(ProtocolKey.CloudEventsKey.SUBJECT, topic); + } +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/build.gradle b/eventmesh-connectors/eventmesh-connector-mcp/build.gradle index 84a228a28d..6a719b7332 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/build.gradle +++ b/eventmesh-connectors/eventmesh-connector-mcp/build.gradle @@ -1,19 +1,32 @@ -plugins { - id 'java' -} - -group = 'org.apache.eventmesh' -version = '1.11.0-release' - -repositories { - mavenCentral() -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ dependencies { - testImplementation platform('org.junit:junit-bom:5.10.0') - testImplementation 'org.junit.jupiter:junit-jupiter' + implementation project(":eventmesh-protocol-plugin:eventmesh-protocol-api") + implementation "io.cloudevents:cloudevents-core" + implementation "com.google.guava:guava" + implementation "io.cloudevents:cloudevents-json-jackson" + implementation ("io.grpc:grpc-protobuf:1.68.0") { + exclude group: "com.google.protobuf", module: "protobuf-java" + } + implementation("com.google.protobuf:protobuf-java:3.25.4") + implementation 'io.netty:netty-common:4.1.114.Final' + implementation 'io.netty:netty-buffer:4.1.114.Final' + implementation 'io.netty:netty-transport:4.1.114.Final' + implementation 'io.netty:netty-handler:4.1.114.Final' + implementation 'io.netty:netty-codec-http:4.1.114.Final' } - -test { - useJUnitPlatform() -} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java index d3eb1e733d..3aca773183 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java @@ -1,6 +1,7 @@ package org.apache.eventmesh.connector.mcp.server; import org.apache.eventmesh.connector.mcp.config.McpServerConfig; +import org.apache.eventmesh.connector.mcp.sink.McpSinkConnector; import org.apache.eventmesh.connector.mcp.source.McpSourceConnector; import org.apache.eventmesh.openconnect.Application; import org.apache.eventmesh.openconnect.util.ConfigUtil; @@ -13,5 +14,10 @@ public static void main(String[] args) throws Exception { Application mcpSourceApp = new Application(); mcpSourceApp.run(McpSourceConnector.class); } + + if (serverConfig.isSinkEnable()) { + Application httpSinkApp = new Application(); + httpSinkApp.run(McpSinkConnector.class); + } } } diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSession.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSession.java index 397988ebaa..460fd1dcac 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSession.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSession.java @@ -1,5 +1,62 @@ package org.apache.eventmesh.connector.mcp.session; -// 会话对象(上下文、历史消息等) +import io.netty.channel.Channel; + +import java.time.Instant; +import java.util.LinkedList; +import java.util.Queue; + public class McpSession { + + private final String sessionId; + + // 与客户端保持通信的通道 + private Channel channel; + + // 最近活跃时间,用于超时清理 + private Instant lastActiveTime; + + // 如果是 Stream 模式,可以缓存未发出的数据块 + private final Queue streamBuffer = new LinkedList<>(); + + public McpSession(String sessionId, Channel channel) { + this.sessionId = sessionId; + this.channel = channel; + this.lastActiveTime = Instant.now(); + } + + public String getSessionId() { + return sessionId; + } + + public Channel getChannel() { + return channel; + } + + public void setChannel(Channel channel) { + this.channel = channel; + touch(); + } + + public void addToBuffer(Object data) { + streamBuffer.add(data); + } + + public Queue getStreamBuffer() { + return streamBuffer; + } + + public Instant getLastActiveTime() { + return lastActiveTime; + } + + public void touch() { + this.lastActiveTime = Instant.now(); + } + + public void close() { + if (channel != null && channel.isOpen()) { + channel.close(); + } + } } diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSessionManager.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSessionManager.java index 8defefd6f5..5163d822de 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSessionManager.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSessionManager.java @@ -1,5 +1,58 @@ package org.apache.eventmesh.connector.mcp.session; -// 管理会话Map/Redis +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + public class McpSessionManager { + + private static final long SESSION_TIMEOUT_SECONDS = 300; + + private static final McpSessionManager INSTANCE = new McpSessionManager(); + + private final Map sessionMap = new ConcurrentHashMap<>(); + + private McpSessionManager() { + } + + public static McpSessionManager getInstance() { + return INSTANCE; + } + + public McpSession getOrCreateSession(String sessionId, io.netty.channel.Channel channel) { + return sessionMap.compute(sessionId, (id, existing) -> { + if (existing != null) { + existing.setChannel(channel); // 恢复连接 + return existing; + } else { + return new McpSession(sessionId, channel); + } + }); + } + + public McpSession getSession(String sessionId) { + return sessionMap.get(sessionId); + } + + public void closeSession(String sessionId) { + McpSession session = sessionMap.remove(sessionId); + if (session != null) { + session.close(); + } + } + + public void clearTimeoutSessions() { + Instant now = Instant.now(); + for (Map.Entry entry : sessionMap.entrySet()) { + if (Duration.between(entry.getValue().getLastActiveTime(), now).getSeconds() > SESSION_TIMEOUT_SECONDS) { + closeSession(entry.getKey()); + } + } + } + + public int activeSessionCount() { + return sessionMap.size(); + } } + diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java index adc218823b..ed3b4b4ea3 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java @@ -24,16 +24,13 @@ import org.apache.eventmesh.common.config.connector.Config; import org.apache.eventmesh.common.config.connector.http.HttpSinkConfig; import org.apache.eventmesh.common.config.connector.http.SinkConnectorConfig; -import org.apache.eventmesh.connector.http.sink.handler.HttpSinkHandler; -import org.apache.eventmesh.connector.http.sink.handler.impl.CommonHttpSinkHandler; -import org.apache.eventmesh.connector.http.sink.handler.impl.HttpSinkHandlerRetryWrapper; -import org.apache.eventmesh.connector.http.sink.handler.impl.WebhookHttpSinkHandler; +import org.apache.eventmesh.connector.mcp.sink.handler.McpSinkHandler; import org.apache.eventmesh.openconnect.api.ConnectorCreateService; import org.apache.eventmesh.openconnect.api.connector.ConnectorContext; import org.apache.eventmesh.openconnect.api.connector.SinkConnectorContext; import org.apache.eventmesh.openconnect.api.sink.Sink; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; - +import org.apache.eventmesh.common.config.connector.mcp.*; import java.util.List; import java.util.Objects; import java.util.concurrent.LinkedBlockingQueue; @@ -47,7 +44,7 @@ public class McpSinkConnector implements Sink, ConnectorCreateService { private HttpSinkConfig httpSinkConfig; @Getter - private HttpSinkHandler sinkHandler; + private McpSinkHandler sinkHandler; private ThreadPoolExecutor executor; diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java index 2e3287a245..edc3194249 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java @@ -19,7 +19,6 @@ import lombok.AllArgsConstructor; import lombok.Data; -import org.apache.eventmesh.connector.http.sink.data.HttpExportRecord; import java.io.Serializable; import java.util.List; @@ -37,6 +36,6 @@ public class McpExportRecordPage implements Serializable { private int pageSize; - private List pageItems; + private List pageItems; } diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/MultiMcpRequestContext.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/MultiMcpRequestContext.java index 1d6ec226a1..a775c60534 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/MultiMcpRequestContext.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/MultiMcpRequestContext.java @@ -17,7 +17,8 @@ package org.apache.eventmesh.connector.mcp.sink.data; -import org.apache.eventmesh.connector.http.sink.data.HttpAttemptEvent; +import lombok.Getter; +import lombok.Setter; import java.util.concurrent.atomic.AtomicInteger; @@ -38,7 +39,9 @@ public class MultiMcpRequestContext { * The last failed event. * If retries occur but still fail, it will be logged, and only the last one will be retained. */ - private HttpAttemptEvent lastFailedEvent; + @Getter + @Setter + private McpAttemptEvent lastFailedEvent; public MultiMcpRequestContext(int remainingEvents) { this.remainingRequests = new AtomicInteger(remainingEvents); @@ -64,11 +67,4 @@ public int getRemainingRequests() { return remainingRequests.get(); } - public HttpAttemptEvent getLastFailedEvent() { - return lastFailedEvent; - } - - public void setLastFailedEvent(HttpAttemptEvent lastFailedEvent) { - this.lastFailedEvent = lastFailedEvent; - } } diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolAdaptor.java b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolAdaptor.java index 7dbd33fee2..bec704046e 100644 --- a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolAdaptor.java +++ b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolAdaptor.java @@ -1,113 +1,70 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.protocol.mcp; import com.fasterxml.jackson.core.type.TypeReference; import io.cloudevents.CloudEvent; import org.apache.eventmesh.common.Constants; import org.apache.eventmesh.common.protocol.ProtocolTransportObject; -import org.apache.eventmesh.common.protocol.http.HttpEventWrapper; -import org.apache.eventmesh.common.protocol.http.common.RequestURI; import org.apache.eventmesh.common.utils.JsonUtils; import org.apache.eventmesh.protocol.api.ProtocolAdaptor; import org.apache.eventmesh.protocol.api.exception.ProtocolHandleException; -import org.apache.eventmesh.protocol.http.resolver.HttpRequestProtocolResolver; +import org.apache.eventmesh.protocol.mcp.resolver.McpRequestProtocolResolver; +import org.apache.eventmesh.common.protocol.mcp.McpEventWrapper; import java.nio.charset.StandardCharsets; import java.util.*; -import static org.apache.eventmesh.protocol.http.HttpProtocolConstant.*; +import static org.apache.eventmesh.protocol.mcp.McpProtocolConstant.CONSTANTS_KEY_BODY; +import static org.apache.eventmesh.protocol.mcp.McpProtocolConstant.CONSTANTS_KEY_HEADERS; -/** - * CloudEvents protocol adaptor, used to transform CloudEvents message to CloudEvents message. - * - * @since 1.3.0 - */ public class McpProtocolAdaptor - implements ProtocolAdaptor { + implements ProtocolAdaptor { @Override public CloudEvent toCloudEvent(ProtocolTransportObject protocolTransportObject) throws ProtocolHandleException { - - if (protocolTransportObject instanceof HttpEventWrapper) { - HttpEventWrapper httpEventWrapper = (HttpEventWrapper) protocolTransportObject; - String requestURI = httpEventWrapper.getRequestURI(); - - return deserializeProtocol(requestURI, httpEventWrapper); - - } else { - throw new ProtocolHandleException(String.format("protocol class: %s", protocolTransportObject.getClass())); - } - } - - private CloudEvent deserializeProtocol(String requestURI, HttpEventWrapper httpEventWrapper) throws ProtocolHandleException { - - if (requestURI.startsWith(RequestURI.PUBLISH.getRequestURI()) || requestURI.startsWith(RequestURI.PUBLISH_BRIDGE.getRequestURI())) { - return HttpRequestProtocolResolver.buildEvent(httpEventWrapper); + if (protocolTransportObject instanceof McpEventWrapper) { + McpEventWrapper wrapper = (McpEventWrapper) protocolTransportObject; + return McpRequestProtocolResolver.buildEvent(wrapper); } else { - throw new ProtocolHandleException(String.format("unsupported requestURI: %s", requestURI)); + throw new ProtocolHandleException("Unsupported protocol: " + protocolTransportObject.getClass()); } - } @Override - public List toBatchCloudEvent(ProtocolTransportObject protocol) - throws ProtocolHandleException { - return Collections.emptyList(); + public List toBatchCloudEvent(ProtocolTransportObject protocol) throws ProtocolHandleException { + return Collections.emptyList(); // 可支持批处理扩展 } @Override public ProtocolTransportObject fromCloudEvent(CloudEvent cloudEvent) throws ProtocolHandleException { - HttpEventWrapper httpEventWrapper = new HttpEventWrapper(); + McpEventWrapper wrapper = new McpEventWrapper(); + Map sysHeaderMap = new HashMap<>(); - // ce attributes - Set attributeNames = cloudEvent.getAttributeNames(); - // ce extensions - Set extensionNames = cloudEvent.getExtensionNames(); - for (String attributeName : attributeNames) { - sysHeaderMap.put(attributeName, cloudEvent.getAttribute(attributeName)); + for (String attr : cloudEvent.getAttributeNames()) { + sysHeaderMap.put(attr, cloudEvent.getAttribute(attr)); } - for (String extensionName : extensionNames) { - sysHeaderMap.put(extensionName, cloudEvent.getExtension(extensionName)); + for (String ext : cloudEvent.getExtensionNames()) { + sysHeaderMap.put(ext, cloudEvent.getExtension(ext)); } - httpEventWrapper.setSysHeaderMap(sysHeaderMap); - // ce data + wrapper.setSysHeaderMap(sysHeaderMap); + if (cloudEvent.getData() != null) { Map dataContentMap = JsonUtils.parseTypeReferenceObject( - new String(Objects.requireNonNull(cloudEvent.getData()).toBytes(), Constants.DEFAULT_CHARSET), - new TypeReference>() { - }); - String requestHeader = JsonUtils.toJSONString( - Objects.requireNonNull(dataContentMap, "Headers must not be null").get(CONSTANTS_KEY_HEADERS)); - byte[] requestBody = Objects.requireNonNull( - JsonUtils.toJSONString(dataContentMap.get(CONSTANTS_KEY_BODY)), "Body must not be null").getBytes(StandardCharsets.UTF_8); - Map requestHeaderMap = JsonUtils.parseTypeReferenceObject(requestHeader, new TypeReference>() { - }); - String requestURI = dataContentMap.get(CONSTANTS_KEY_PATH).toString(); - String httpMethod = dataContentMap.get(CONSTANTS_KEY_METHOD).toString(); + new String(Objects.requireNonNull(cloudEvent.getData()).toBytes(), Constants.DEFAULT_CHARSET), + new TypeReference>() {} + ); + + String rawHeader = JsonUtils.toJSONString(dataContentMap.get(CONSTANTS_KEY_HEADERS)); + byte[] body = JsonUtils.toJSONString(dataContentMap.get(CONSTANTS_KEY_BODY)).getBytes(StandardCharsets.UTF_8); + + Map headerMap = JsonUtils.parseTypeReferenceObject( + rawHeader, new TypeReference>() {} + ); - httpEventWrapper.setHeaderMap(requestHeaderMap); - httpEventWrapper.setBody(requestBody); - httpEventWrapper.setRequestURI(requestURI); - httpEventWrapper.setHttpMethod(httpMethod); + wrapper.setHeaderMap(headerMap); + wrapper.setBody(body); } - return httpEventWrapper; + return wrapper; } @Override diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolConstant.java b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolConstant.java index 1c42cc190b..0b31515b57 100644 --- a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolConstant.java +++ b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolConstant.java @@ -20,23 +20,26 @@ public enum McpProtocolConstant { ; - public static final String PROTOCOL_NAME = "http"; - - public static final String CONSTANTS_DEFAULT_SOURCE = "/"; - public static final String CONSTANTS_DEFAULT_TYPE = "http_request"; - public static final String CONSTANTS_DEFAULT_SUBJECT = ""; + public static final String PROTOCOL_NAME = "mcp"; public static final String CONSTANTS_KEY_ID = "id"; public static final String CONSTANTS_KEY_SOURCE = "source"; public static final String CONSTANTS_KEY_TYPE = "type"; public static final String CONSTANTS_KEY_SUBJECT = "subject"; - public static final String CONSTANTS_KEY_HEADERS = "headers"; public static final String CONSTANTS_KEY_BODY = "body"; - public static final String CONSTANTS_KEY_PATH = "path"; - public static final String CONSTANTS_KEY_METHOD = "method"; + public static final String CONSTANTS_KEY_HEADERS = "headers"; + public static final String CONSTANTS_KEY_TIMESTAMP = "timestamp"; + public static final String CONSTANTS_KEY_SCHEMA = "schema"; + public static final String CONSTANTS_KEY_CONTEXT = "context"; + + public static final String CONSTANTS_HEADER_SESSION_ID = "Mcp-Session-Id"; + public static final String CONSTANTS_HEADER_STREAM_ID = "Mcp-Stream-Id"; - public static final String DATA_CONTENT_TYPE = "Content-Type"; + public static final String CONSTANTS_DEFAULT_TYPE = "mcp_request"; + public static final String CONSTANTS_DEFAULT_SOURCE = "/"; + public static final String CONSTANTS_DEFAULT_SUBJECT = ""; - public static final String APPLICATION_JSON = "application/json"; - public static final String PROTOBUF = "application/x-protobuf"; + public static final String CONSTANTS_APPLICATION_JSON = "application/json"; + public static final String CONSTANTS_APPLICATION_PROTOBUF = "application/x-protobuf"; + public static final String CONSTANTS_DATA_CONTENT_TYPE = "Content-Type"; } diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/resolver/McpRequestProtocolResolver.java b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/resolver/McpRequestProtocolResolver.java index d26b977cdf..96fd5d877d 100644 --- a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/resolver/McpRequestProtocolResolver.java +++ b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/resolver/McpRequestProtocolResolver.java @@ -6,7 +6,7 @@ * (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 + * Mcp://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, @@ -21,10 +21,10 @@ import io.cloudevents.CloudEvent; import io.cloudevents.core.v1.CloudEventBuilder; import org.apache.commons.lang3.StringUtils; -import org.apache.eventmesh.common.protocol.http.HttpEventWrapper; import org.apache.eventmesh.common.utils.JsonUtils; import org.apache.eventmesh.protocol.api.exception.ProtocolHandleException; -import org.apache.eventmesh.protocol.http.HttpProtocolConstant; +import org.apache.eventmesh.protocol.mcp.McpProtocolConstant; +import org.apache.eventmesh.common.protocol.mcp.McpEventWrapper; import java.net.URI; import java.nio.charset.StandardCharsets; @@ -35,70 +35,64 @@ public class McpRequestProtocolResolver { - public static CloudEvent buildEvent(HttpEventWrapper httpEventWrapper) throws ProtocolHandleException { - + public static CloudEvent buildEvent(McpEventWrapper mcpEventWrapper) throws ProtocolHandleException { try { CloudEventBuilder builder = new CloudEventBuilder(); - Map requestHeaderMap = httpEventWrapper.getHeaderMap(); - - Map sysHeaderMap = httpEventWrapper.getSysHeaderMap(); - - String id = sysHeaderMap.getOrDefault(HttpProtocolConstant.CONSTANTS_KEY_ID, UUID.randomUUID()).toString(); + Map requestHeaderMap = mcpEventWrapper.getHeaderMap(); + Map sysHeaderMap = mcpEventWrapper.getSysHeaderMap(); - String source = sysHeaderMap.getOrDefault(HttpProtocolConstant.CONSTANTS_KEY_SOURCE, - HttpProtocolConstant.CONSTANTS_DEFAULT_SOURCE).toString(); + String id = sysHeaderMap.getOrDefault(McpProtocolConstant.CONSTANTS_KEY_ID, UUID.randomUUID()).toString(); + String source = sysHeaderMap.getOrDefault(McpProtocolConstant.CONSTANTS_KEY_SOURCE, + McpProtocolConstant.CONSTANTS_DEFAULT_SOURCE).toString(); + String type = sysHeaderMap.getOrDefault(McpProtocolConstant.CONSTANTS_KEY_TYPE, + McpProtocolConstant.CONSTANTS_DEFAULT_TYPE).toString(); + String subject = sysHeaderMap.getOrDefault(McpProtocolConstant.CONSTANTS_KEY_SUBJECT, + McpProtocolConstant.CONSTANTS_DEFAULT_SUBJECT).toString(); - String type = sysHeaderMap.getOrDefault(HttpProtocolConstant.CONSTANTS_KEY_TYPE, - HttpProtocolConstant.CONSTANTS_DEFAULT_TYPE).toString(); + String dataContentType = requestHeaderMap.getOrDefault(McpProtocolConstant.CONSTANTS_DATA_CONTENT_TYPE, + McpProtocolConstant.CONSTANTS_APPLICATION_JSON).toString(); - String subject = sysHeaderMap.getOrDefault(HttpProtocolConstant.CONSTANTS_KEY_SUBJECT, - HttpProtocolConstant.CONSTANTS_DEFAULT_SUBJECT).toString(); - - String dataContentType = requestHeaderMap.getOrDefault(HttpProtocolConstant.DATA_CONTENT_TYPE, - HttpProtocolConstant.APPLICATION_JSON).toString(); - // with attributes + // Set basic CloudEvent attributes builder.withId(id) - .withType(type) - .withSource(URI.create(HttpProtocolConstant.CONSTANTS_KEY_SOURCE + ":" + source)) - .withSubject(subject) - .withDataContentType(dataContentType); - - // with extensions - for (Map.Entry extension : sysHeaderMap.entrySet()) { - if (StringUtils.equals(HttpProtocolConstant.CONSTANTS_KEY_ID, extension.getKey()) - || StringUtils.equals(HttpProtocolConstant.CONSTANTS_KEY_SOURCE, extension.getKey()) - || StringUtils.equals(HttpProtocolConstant.CONSTANTS_KEY_TYPE, extension.getKey()) - || StringUtils.equals(HttpProtocolConstant.CONSTANTS_KEY_SUBJECT, extension.getKey())) { + .withType(type) + .withSource(URI.create(source)) + .withSubject(subject) + .withDataContentType(dataContentType); + + // Set extensions + for (Map.Entry entry : sysHeaderMap.entrySet()) { + String key = entry.getKey(); + if (key.equalsIgnoreCase(McpProtocolConstant.CONSTANTS_KEY_ID) + || key.equalsIgnoreCase(McpProtocolConstant.CONSTANTS_KEY_SOURCE) + || key.equalsIgnoreCase(McpProtocolConstant.CONSTANTS_KEY_TYPE) + || key.equalsIgnoreCase(McpProtocolConstant.CONSTANTS_KEY_SUBJECT)) { continue; } - String lowerExtensionKey = extension.getKey().toLowerCase(Locale.getDefault()); - builder.withExtension(lowerExtensionKey, sysHeaderMap.get(extension.getKey()).toString()); + builder.withExtension(key.toLowerCase(Locale.ROOT), entry.getValue().toString()); } - byte[] requestBody = httpEventWrapper.getBody(); - - if (StringUtils.equals(dataContentType, HttpProtocolConstant.APPLICATION_JSON)) { - Map requestBodyMap = JsonUtils.parseTypeReferenceObject(new String(requestBody), - new TypeReference>() { - }); - - String requestURI = httpEventWrapper.getRequestURI(); + // Handle body + byte[] requestBody = mcpEventWrapper.getBody(); + if (StringUtils.equals(dataContentType, McpProtocolConstant.CONSTANTS_APPLICATION_JSON)) { + Map requestBodyMap = JsonUtils.parseTypeReferenceObject( + new String(requestBody, StandardCharsets.UTF_8), + new TypeReference>() {} + ); Map data = new HashMap<>(); - data.put(HttpProtocolConstant.CONSTANTS_KEY_HEADERS, requestHeaderMap); - data.put(HttpProtocolConstant.CONSTANTS_KEY_BODY, requestBodyMap); - data.put(HttpProtocolConstant.CONSTANTS_KEY_PATH, requestURI); - data.put(HttpProtocolConstant.CONSTANTS_KEY_METHOD, httpEventWrapper.getHttpMethod()); - // with data - builder = builder.withData(JsonUtils.toJSONString(data).getBytes(StandardCharsets.UTF_8)); - } else if (StringUtils.equals(dataContentType, HttpProtocolConstant.PROTOBUF)) { - // with data - builder = builder.withData(requestBody); + data.put(McpProtocolConstant.CONSTANTS_KEY_HEADERS, requestHeaderMap); + data.put(McpProtocolConstant.CONSTANTS_KEY_BODY, requestBodyMap); + + builder.withData(JsonUtils.toJSONString(data).getBytes(StandardCharsets.UTF_8)); + } else { + builder.withData(requestBody); } + return builder.build(); } catch (Exception e) { - throw new ProtocolHandleException(e.getMessage(), e); + throw new ProtocolHandleException("Failed to build CloudEvent from McpEventWrapper", e); } } + } diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/session/McpSession.java b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/session/McpSession.java deleted file mode 100644 index b31c7dae79..0000000000 --- a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/session/McpSession.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.apache.eventmesh.protocol.mcp.session; - -public class McpSession { -} diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/session/McpSessionManager.java b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/session/McpSessionManager.java deleted file mode 100644 index 6e7fd810c0..0000000000 --- a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/session/McpSessionManager.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.apache.eventmesh.protocol.mcp.session; - -public class McpSessionManager { -} diff --git a/settings.gradle b/settings.gradle index 327ca7e1a2..80da648d5e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -107,6 +107,7 @@ include 'eventmesh-protocol-plugin:eventmesh-protocol-meshmessage' include 'eventmesh-protocol-plugin:eventmesh-protocol-http' include 'eventmesh-protocol-plugin:eventmesh-protocol-grpc' include 'eventmesh-protocol-plugin:eventmesh-protocol-grpcmessage' +include 'eventmesh-protocol-plugin:eventmesh-protocol-mcp' include 'eventmesh-metrics-plugin' include 'eventmesh-metrics-plugin:eventmesh-metrics-api' From 996f1fc4b3a722b72038162e8702532e8af43f5c Mon Sep 17 00:00:00 2001 From: jerryyummy <461697582@qq.com> Date: Wed, 2 Jul 2025 23:10:45 +0800 Subject: [PATCH 04/36] build the basic mcp server without streamable http --- .../eventmesh-connector-mcp/build.gradle | 15 + .../mcp/server/McpConnectServer.java | 6 - .../connector/mcp/sink/McpSinkConnector.java | 171 ----------- .../mcp/sink/data/McpAttemptEvent.java | 122 -------- .../mcp/sink/data/McpConnectRecord.java | 117 -------- .../mcp/sink/data/McpExportMetadata.java | 48 ---- .../mcp/sink/data/McpExportRecord.java | 37 --- .../mcp/sink/data/McpExportRecordPage.java | 41 --- .../mcp/sink/data/MultiMcpRequestContext.java | 70 ----- .../sink/handler/AbstractMcpSinkHandler.java | 101 ------- .../mcp/sink/handler/McpDeliveryStrategy.java | 23 -- .../mcp/sink/handler/McpSinkHandler.java | 75 ----- .../handler/impl/CommonMcpSinkHandler.java | 268 ------------------ .../impl/McpSinkHandlerRetryWrapper.java | 118 -------- .../mcp/source/McpSourceConnector.java | 15 +- .../connector/mcp/source/data/McpRequest.java | 2 +- .../protocol/impl/McpStandardProtocol.java | 17 +- .../src/main/resources/source-config.yml | 2 +- 18 files changed, 34 insertions(+), 1214 deletions(-) delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpAttemptEvent.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/MultiMcpRequestContext.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpDeliveryStrategy.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java diff --git a/eventmesh-connectors/eventmesh-connector-mcp/build.gradle b/eventmesh-connectors/eventmesh-connector-mcp/build.gradle index 6a719b7332..ff9d734208 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/build.gradle +++ b/eventmesh-connectors/eventmesh-connector-mcp/build.gradle @@ -16,6 +16,9 @@ */ dependencies { + api project(":eventmesh-openconnect:eventmesh-openconnect-java") + implementation project(":eventmesh-common") + implementation project(":eventmesh-protocol-plugin:eventmesh-protocol-api") implementation "io.cloudevents:cloudevents-core" implementation "com.google.guava:guava" @@ -23,10 +26,22 @@ dependencies { implementation ("io.grpc:grpc-protobuf:1.68.0") { exclude group: "com.google.protobuf", module: "protobuf-java" } + implementation 'io.cloudevents:cloudevents-http-vertx:3.0.0' + implementation 'io.vertx:vertx-web:4.5.8' + implementation 'io.vertx:vertx-web-client:4.5.9' + implementation 'dev.failsafe:failsafe:3.3.2' + + + testImplementation 'org.apache.httpcomponents.client5:httpclient5:5.4' + testImplementation 'org.apache.httpcomponents.client5:httpclient5-fluent:5.4' + testImplementation 'org.mock-server:mockserver-netty:5.15.0' + implementation("com.google.protobuf:protobuf-java:3.25.4") implementation 'io.netty:netty-common:4.1.114.Final' implementation 'io.netty:netty-buffer:4.1.114.Final' implementation 'io.netty:netty-transport:4.1.114.Final' implementation 'io.netty:netty-handler:4.1.114.Final' implementation 'io.netty:netty-codec-http:4.1.114.Final' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' } diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java index 3aca773183..d3eb1e733d 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java @@ -1,7 +1,6 @@ package org.apache.eventmesh.connector.mcp.server; import org.apache.eventmesh.connector.mcp.config.McpServerConfig; -import org.apache.eventmesh.connector.mcp.sink.McpSinkConnector; import org.apache.eventmesh.connector.mcp.source.McpSourceConnector; import org.apache.eventmesh.openconnect.Application; import org.apache.eventmesh.openconnect.util.ConfigUtil; @@ -14,10 +13,5 @@ public static void main(String[] args) throws Exception { Application mcpSourceApp = new Application(); mcpSourceApp.run(McpSourceConnector.class); } - - if (serverConfig.isSinkEnable()) { - Application httpSinkApp = new Application(); - httpSinkApp.run(McpSinkConnector.class); - } } } diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java deleted file mode 100644 index ed3b4b4ea3..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink; - -import lombok.Getter; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.apache.eventmesh.common.EventMeshThreadFactory; -import org.apache.eventmesh.common.config.connector.Config; -import org.apache.eventmesh.common.config.connector.http.HttpSinkConfig; -import org.apache.eventmesh.common.config.connector.http.SinkConnectorConfig; -import org.apache.eventmesh.connector.mcp.sink.handler.McpSinkHandler; -import org.apache.eventmesh.openconnect.api.ConnectorCreateService; -import org.apache.eventmesh.openconnect.api.connector.ConnectorContext; -import org.apache.eventmesh.openconnect.api.connector.SinkConnectorContext; -import org.apache.eventmesh.openconnect.api.sink.Sink; -import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; -import org.apache.eventmesh.common.config.connector.mcp.*; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -@Slf4j -public class McpSinkConnector implements Sink, ConnectorCreateService { - - private HttpSinkConfig httpSinkConfig; - - @Getter - private McpSinkHandler sinkHandler; - - private ThreadPoolExecutor executor; - - private final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(10000); - - private final AtomicBoolean isStart = new AtomicBoolean(true); - - @Override - public Class configClass() { - return HttpSinkConfig.class; - } - - @Override - public Sink create() { - return new McpSinkConnector(); - } - - @Override - public void init(Config config) throws Exception { - this.httpSinkConfig = (HttpSinkConfig) config; - doInit(); - } - - @Override - public void init(ConnectorContext connectorContext) throws Exception { - SinkConnectorContext sinkConnectorContext = (SinkConnectorContext) connectorContext; - this.httpSinkConfig = (HttpSinkConfig) sinkConnectorContext.getSinkConfig(); - doInit(); - } - - @SneakyThrows - private void doInit() { - // Fill default values if absent - SinkConnectorConfig.populateFieldsWithDefaults(this.httpSinkConfig.connectorConfig); - // Create different handlers for different configurations - HttpSinkHandler nonRetryHandler; - if (this.httpSinkConfig.connectorConfig.getWebhookConfig().isActivate()) { - nonRetryHandler = new WebhookHttpSinkHandler(this.httpSinkConfig.connectorConfig); - } else { - nonRetryHandler = new CommonHttpSinkHandler(this.httpSinkConfig.connectorConfig); - } - - int maxRetries = this.httpSinkConfig.connectorConfig.getRetryConfig().getMaxRetries(); - if (maxRetries == 0) { - // Use the original sink handler - this.sinkHandler = nonRetryHandler; - } else if (maxRetries > 0) { - // Wrap the sink handler with a retry handler - this.sinkHandler = new HttpSinkHandlerRetryWrapper(this.httpSinkConfig.connectorConfig, nonRetryHandler); - } else { - throw new IllegalArgumentException("Max retries must be greater than or equal to 0."); - } - boolean isParallelized = this.httpSinkConfig.connectorConfig.isParallelized(); - int parallelism = isParallelized ? this.httpSinkConfig.connectorConfig.getParallelism() : 1; - executor = new ThreadPoolExecutor(parallelism, parallelism, 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<>(), new EventMeshThreadFactory("http-sink-handler")); - } - - @Override - public void start() throws Exception { - this.sinkHandler.start(); - for (int i = 0; i < this.httpSinkConfig.connectorConfig.getParallelism(); i++) { - executor.execute(() -> { - while (isStart.get()) { - ConnectRecord connectRecord = null; - try { - connectRecord = queue.poll(2, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - if (connectRecord != null) { - sinkHandler.handle(connectRecord); - } - } - }); - } - } - - @Override - public void commit(ConnectRecord record) { - - } - - @Override - public String name() { - return this.httpSinkConfig.connectorConfig.getConnectorName(); - } - - @Override - public void onException(ConnectRecord record) { - - } - - @Override - public void stop() throws Exception { - isStart.set(false); - while (!queue.isEmpty()) { - ConnectRecord record = queue.poll(); - this.sinkHandler.handle(record); - } - try { - Thread.sleep(50); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - this.sinkHandler.stop(); - log.info("All tasks completed, start shut down http sink connector"); - } - - @Override - public void put(List sinkRecords) { - for (ConnectRecord sinkRecord : sinkRecords) { - try { - if (Objects.isNull(sinkRecord)) { - log.warn("ConnectRecord data is null, ignore."); - continue; - } - queue.put(sinkRecord); - } catch (Exception e) { - log.error("Failed to sink message via HTTP. ", e); - } - } - } -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpAttemptEvent.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpAttemptEvent.java deleted file mode 100644 index 5697dff4b3..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpAttemptEvent.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; - -import java.util.concurrent.atomic.AtomicInteger; - -/** - * Single HTTP attempt event - */ -public class McpAttemptEvent { - - public static final String PREFIX = "http-attempt-event-"; - - private final int maxAttempts; - - private final AtomicInteger attempts; - - private Throwable lastException; - - - public McpAttemptEvent(int maxAttempts) { - this.maxAttempts = maxAttempts; - this.attempts = new AtomicInteger(0); - } - - /** - * Increment the attempts - */ - public void incrementAttempts() { - attempts.incrementAndGet(); - } - - /** - * Update the event, incrementing the attempts and setting the last exception - * - * @param exception the exception to update, can be null - */ - public void updateEvent(Throwable exception) { - // increment the attempts - incrementAttempts(); - - // update the last exception - lastException = exception; - } - - /** - * Check if the attempts are less than the maximum attempts - * - * @return true if the attempts are less than the maximum attempts, false otherwise - */ - public boolean canAttempt() { - return attempts.get() < maxAttempts; - } - - public boolean isComplete() { - if (attempts.get() == 0) { - // No start yet - return false; - } - - // If no attempt can be made or the last exception is null, the event completed - return !canAttempt() || lastException == null; - } - - - public int getMaxAttempts() { - return maxAttempts; - } - - public int getAttempts() { - return attempts.get(); - } - - public Throwable getLastException() { - return lastException; - } - - /** - * Get the limited exception message with the default limit of 256 - * - * @return the limited exception message - */ - public String getLimitedExceptionMessage() { - return getLimitedExceptionMessage(256); - } - - /** - * Get the limited exception message with the specified limit - * - * @param maxLimit the maximum limit of the exception message - * @return the limited exception message - */ - public String getLimitedExceptionMessage(int maxLimit) { - if (lastException == null) { - return ""; - } - String message = lastException.getMessage(); - if (message == null) { - return ""; - } - if (message.length() > maxLimit) { - return message.substring(0, maxLimit); - } - return message; - } - -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java deleted file mode 100644 index 1750fb7ba4..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; - -import lombok.Builder; -import lombok.Getter; -import org.apache.eventmesh.common.remote.offset.http.HttpRecordOffset; -import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; -import org.apache.eventmesh.openconnect.offsetmgmt.api.data.KeyValue; - -import java.io.Serializable; -import java.time.LocalDateTime; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -/** - * a special ConnectRecord for HttpSinkConnector - */ -@Getter -@Builder -public class McpConnectRecord implements Serializable { - - private static final long serialVersionUID = 5271462532332251473L; - - /** - * The unique identifier for the HttpConnectRecord - */ - private final String httpRecordId = UUID.randomUUID().toString(); - - /** - * The time when the HttpConnectRecord was created - */ - private LocalDateTime createTime; - - /** - * The type of the HttpConnectRecord - */ - private String type; - - /** - * The event id of the HttpConnectRecord - */ - private String eventId; - - private Object data; - - private KeyValue extensions; - - @Override - public String toString() { - return "HttpConnectRecord{" - + "createTime=" + createTime - + ", httpRecordId='" + httpRecordId - + ", type='" + type - + ", eventId='" + eventId - + ", data=" + data - + ", extensions=" + extensions - + '}'; - } - - /** - * Convert ConnectRecord to HttpConnectRecord - * - * @param record the ConnectRecord to convert - * @return the converted HttpConnectRecord - */ - public static McpConnectRecord convertConnectRecord(ConnectRecord record, String type) { - Map offsetMap = new HashMap<>(); - if (record != null && record.getPosition() != null && record.getPosition().getRecordOffset() != null) { - if (HttpRecordOffset.class.equals(record.getPosition().getRecordOffsetClazz())) { - offsetMap = ((HttpRecordOffset) record.getPosition().getRecordOffset()).getOffsetMap(); - } - } - String offset = "0"; - if (!offsetMap.isEmpty()) { - offset = offsetMap.values().iterator().next().toString(); - } - if (record.getData() instanceof byte[]) { - String data = Base64.getEncoder().encodeToString((byte[]) record.getData()); - record.addExtension("isBase64", true); - return McpConnectRecord.builder() - .type(type) - .createTime(LocalDateTime.now()) - .eventId(type + "-" + offset) - .data(data) - .extensions(record.getExtensions()) - .build(); - } else { - record.addExtension("isBase64", false); - return McpConnectRecord.builder() - .type(type) - .createTime(LocalDateTime.now()) - .eventId(type + "-" + offset) - .data(record.getData()) - .extensions(record.getExtensions()) - .build(); - } - } - -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java deleted file mode 100644 index d9129cc498..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; - -import lombok.Builder; -import lombok.Data; - -import java.io.Serializable; -import java.time.LocalDateTime; - -/** - * Metadata for an HTTP export operation. - */ -@Data -@Builder -public class McpExportMetadata implements Serializable { - - private static final long serialVersionUID = 1121010466793041920L; - - private String url; - - private int code; - - private String message; - - private LocalDateTime receivedTime; - - private String recordId; - - private String retriedBy; - - private int retryNum; -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java deleted file mode 100644 index 346e054b92..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; - -import lombok.AllArgsConstructor; -import lombok.Data; - -import java.io.Serializable; - -/** - * Represents an HTTP export record containing metadata and data to be exported. - */ -@Data -@AllArgsConstructor -public class McpExportRecord implements Serializable { - - private static final long serialVersionUID = 6010283911452947157L; - - private McpExportMetadata metadata; - - private Object data; -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java deleted file mode 100644 index edc3194249..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; - -import lombok.AllArgsConstructor; -import lombok.Data; - -import java.io.Serializable; -import java.util.List; - -/** - * Represents a page of HTTP export records. - */ -@Data -@AllArgsConstructor -public class McpExportRecordPage implements Serializable { - - private static final long serialVersionUID = 1143791658357035990L; - - private int pageNum; - - private int pageSize; - - private List pageItems; - -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/MultiMcpRequestContext.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/MultiMcpRequestContext.java deleted file mode 100644 index a775c60534..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/MultiMcpRequestContext.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; - -import lombok.Getter; -import lombok.Setter; - -import java.util.concurrent.atomic.AtomicInteger; - - -/** - * Multi HTTP request context - */ -public class MultiMcpRequestContext { - - public static final String NAME = "multi-http-request-context"; - - /** - * The remaining requests to be processed. - */ - private final AtomicInteger remainingRequests; - - /** - * The last failed event. - * If retries occur but still fail, it will be logged, and only the last one will be retained. - */ - @Getter - @Setter - private McpAttemptEvent lastFailedEvent; - - public MultiMcpRequestContext(int remainingEvents) { - this.remainingRequests = new AtomicInteger(remainingEvents); - } - - /** - * Decrement the remaining requests by 1. - */ - public void decrementRemainingRequests() { - remainingRequests.decrementAndGet(); - } - - /** - * Check if all requests have been processed. - * - * @return true if all requests have been processed, false otherwise. - */ - public boolean isAllRequestsProcessed() { - return remainingRequests.get() == 0; - } - - public int getRemainingRequests() { - return remainingRequests.get(); - } - -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java deleted file mode 100644 index 03ae01c9e5..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.handler; - -import lombok.Getter; -import org.apache.eventmesh.common.config.connector.http.SinkConnectorConfig; -import org.apache.eventmesh.connector.http.sink.data.HttpAttemptEvent; -import org.apache.eventmesh.connector.http.sink.data.HttpConnectRecord; -import org.apache.eventmesh.connector.http.sink.data.MultiHttpRequestContext; -import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; - -import java.net.URI; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -/** - * AbstractHttpSinkHandler is an abstract class that provides a base implementation for HttpSinkHandler. - */ -public abstract class AbstractMcpSinkHandler implements McpSinkHandler { - - @Getter - private final SinkConnectorConfig sinkConnectorConfig; - - @Getter - private final List urls; - - private final McpDeliveryStrategy deliveryStrategy; - - private int roundRobinIndex = 0; - - protected AbstractMcpSinkHandler(SinkConnectorConfig sinkConnectorConfig) { - this.sinkConnectorConfig = sinkConnectorConfig; - this.deliveryStrategy = McpDeliveryStrategy.valueOf(sinkConnectorConfig.getDeliveryStrategy()); - // Initialize URLs - String[] urlStrings = sinkConnectorConfig.getUrls(); - this.urls = Arrays.stream(urlStrings) - .map(URI::create) - .collect(Collectors.toList()); - } - - /** - * Processes a ConnectRecord by sending it over HTTP or HTTPS. This method should be called for each ConnectRecord that needs to be processed. - * - * @param record the ConnectRecord to process - */ - @Override - public void handle(ConnectRecord record) { - // build attributes - Map attributes = new ConcurrentHashMap<>(); - - switch (deliveryStrategy) { - case ROUND_ROBIN: - attributes.put(MultiHttpRequestContext.NAME, new MultiHttpRequestContext(1)); - URI url = urls.get(roundRobinIndex); - roundRobinIndex = (roundRobinIndex + 1) % urls.size(); - sendRecordToUrl(record, attributes, url); - break; - case BROADCAST: - attributes.put(MultiHttpRequestContext.NAME, new MultiHttpRequestContext(urls.size())); - // send the record to all URLs - urls.forEach(url0 -> sendRecordToUrl(record, attributes, url0)); - break; - default: - throw new IllegalArgumentException("Unknown delivery strategy: " + deliveryStrategy); - } - } - - private void sendRecordToUrl(ConnectRecord record, Map attributes, URI url) { - // convert ConnectRecord to HttpConnectRecord - String type = String.format("%s.%s.%s", - this.sinkConnectorConfig.getConnectorName(), url.getScheme(), - this.sinkConnectorConfig.getWebhookConfig().isActivate() ? "webhook" : "common"); - HttpConnectRecord httpConnectRecord = HttpConnectRecord.convertConnectRecord(record, type); - - // add AttemptEvent to the attributes - HttpAttemptEvent attemptEvent = new HttpAttemptEvent(this.sinkConnectorConfig.getRetryConfig().getMaxRetries() + 1); - attributes.put(HttpAttemptEvent.PREFIX + httpConnectRecord.getHttpRecordId(), attemptEvent); - - // deliver the record - deliver(url, httpConnectRecord, attributes, record); - } - -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpDeliveryStrategy.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpDeliveryStrategy.java deleted file mode 100644 index 07cbbe3d46..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpDeliveryStrategy.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.handler; - -public enum McpDeliveryStrategy { - ROUND_ROBIN, - BROADCAST -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java deleted file mode 100644 index 02d18e8d2e..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.handler; - -import io.vertx.core.Future; -import io.vertx.core.buffer.Buffer; -import io.vertx.ext.web.client.HttpResponse; -import org.apache.eventmesh.connector.http.sink.data.HttpConnectRecord; -import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; - -import java.net.URI; -import java.util.Map; - -/** - * Interface for handling ConnectRecords via HTTP or HTTPS. Classes implementing this interface are responsible for processing ConnectRecords by - * sending them over HTTP or HTTPS, with additional support for handling multiple requests and asynchronous processing. - * - *

Any class that needs to process ConnectRecords via HTTP or HTTPS should implement this interface. - * Implementing classes must provide implementations for the {@link #start()}, {@link #handle(ConnectRecord)}, - * {@link #deliver(URI, HttpConnectRecord, Map, ConnectRecord)}, and {@link #stop()} methods.

- * - *

Implementing classes should ensure thread safety and handle HTTP/HTTPS communication efficiently. - * The {@link #start()} method initializes any necessary resources for HTTP/HTTPS communication. The {@link #handle(ConnectRecord)} method processes a - * ConnectRecord by sending it over HTTP or HTTPS. The {@link #deliver(URI, HttpConnectRecord, Map, ConnectRecord)} method processes HttpConnectRecord - * on specified URL while returning its own processing logic {@link #stop()} method releases any resources used for HTTP/HTTPS communication.

- * - *

It's recommended to handle exceptions gracefully within the {@link #deliver(URI, HttpConnectRecord, Map, ConnectRecord)} method - * to prevent message loss or processing interruptions.

- */ -public interface McpSinkHandler { - - /** - * Initializes the HTTP/HTTPS handler. This method should be called before using the handler. - */ - void start(); - - /** - * Processes a ConnectRecord by sending it over HTTP or HTTPS. This method should be called for each ConnectRecord that needs to be processed. - * - * @param record the ConnectRecord to process - */ - void handle(ConnectRecord record); - - - /** - * Processes HttpConnectRecord on specified URL while returning its own processing logic - * - * @param url URI to which the HttpConnectRecord should be sent - * @param httpConnectRecord HttpConnectRecord to process - * @param attributes additional attributes to be used in processing - * @return processing chain - */ - Future> deliver(URI url, HttpConnectRecord httpConnectRecord, Map attributes, ConnectRecord connectRecord); - - /** - * Cleans up and releases resources used by the HTTP/HTTPS handler. This method should be called when the handler is no longer needed. - */ - void stop(); -} - diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java deleted file mode 100644 index 0f5e633e42..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.handler.impl; - -import io.netty.handler.codec.http.HttpHeaderNames; -import io.vertx.core.Future; -import io.vertx.core.MultiMap; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpHeaders; -import io.vertx.ext.web.client.HttpResponse; -import io.vertx.ext.web.client.WebClient; -import io.vertx.ext.web.client.WebClientOptions; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.apache.eventmesh.common.config.connector.http.SinkConnectorConfig; -import org.apache.eventmesh.common.utils.JsonUtils; -import org.apache.eventmesh.connector.http.sink.data.HttpAttemptEvent; -import org.apache.eventmesh.connector.http.sink.data.HttpConnectRecord; -import org.apache.eventmesh.connector.http.sink.data.MultiHttpRequestContext; -import org.apache.eventmesh.connector.http.sink.handler.AbstractHttpSinkHandler; -import org.apache.eventmesh.connector.http.util.HttpUtils; -import org.apache.eventmesh.openconnect.offsetmgmt.api.callback.SendExceptionContext; -import org.apache.eventmesh.openconnect.offsetmgmt.api.callback.SendResult; -import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; - -import java.net.URI; -import java.time.ZoneId; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * Common HTTP/HTTPS Sink Handler implementation to handle ConnectRecords by sending them over HTTP or HTTPS to configured URLs. - * - *

This handler initializes a WebClient for making HTTP requests based on the provided SinkConnectorConfig. - * It handles processing ConnectRecords by converting them to HttpConnectRecord and sending them asynchronously to each configured URL using the - * WebClient.

- * - *

The handler uses Vert.x's WebClient to perform HTTP/HTTPS requests. It initializes the WebClient in the {@link #start()} - * method and closes it in the {@link #stop()} method to manage resources efficiently.

- * - *

Each ConnectRecord is processed and sent to all configured URLs concurrently using asynchronous HTTP requests.

- */ -@Slf4j -@Getter -public class CommonMcpSinkHandler extends AbstractHttpSinkHandler { - - private WebClient webClient; - - - public CommonMcpSinkHandler(SinkConnectorConfig sinkConnectorConfig) { - super(sinkConnectorConfig); - } - - /** - * Initializes the WebClient for making HTTP requests based on the provided SinkConnectorConfig. - */ - @Override - public void start() { - // Create WebClient - doInitWebClient(); - } - - /** - * Initializes the WebClient with the provided configuration options. - */ - private void doInitWebClient() { - SinkConnectorConfig sinkConnectorConfig = getSinkConnectorConfig(); - final Vertx vertx = Vertx.vertx(); - WebClientOptions options = new WebClientOptions() - .setKeepAlive(sinkConnectorConfig.isKeepAlive()) - .setKeepAliveTimeout(sinkConnectorConfig.getKeepAliveTimeout() / 1000) - .setIdleTimeout(sinkConnectorConfig.getIdleTimeout()) - .setIdleTimeoutUnit(TimeUnit.MILLISECONDS) - .setConnectTimeout(sinkConnectorConfig.getConnectionTimeout()) - .setMaxPoolSize(sinkConnectorConfig.getMaxConnectionPoolSize()) - .setPipelining(sinkConnectorConfig.isParallelized()); - this.webClient = WebClient.create(vertx, options); - } - - /** - * Processes HttpConnectRecord on specified URL while returning its own processing logic. This method sends the HttpConnectRecord to the specified - * URL using the WebClient. - * - * @param url URI to which the HttpConnectRecord should be sent - * @param httpConnectRecord HttpConnectRecord to process - * @param attributes additional attributes to be used in processing - * @return processing chain - */ - @Override - public Future> deliver(URI url, HttpConnectRecord httpConnectRecord, Map attributes, - ConnectRecord connectRecord) { - // create headers - Map extensionMap = new HashMap<>(); - Set extensionKeySet = httpConnectRecord.getExtensions().keySet(); - for (String extensionKey : extensionKeySet) { - Object v = httpConnectRecord.getExtensions().getObject(extensionKey); - extensionMap.put(extensionKey, v); - } - - MultiMap headers = HttpHeaders.headers() - .set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8") - .set(HttpHeaderNames.ACCEPT, "application/json; charset=utf-8") - .set("extension", JsonUtils.toJSONString(extensionMap)); - // get timestamp and offset - Long timestamp = httpConnectRecord.getCreateTime() - .atZone(ZoneId.systemDefault()) - .toInstant() - .toEpochMilli(); - - // send the request - return this.webClient.post(url.getPath()) - .host(url.getHost()) - .port(url.getPort() == -1 ? (Objects.equals(url.getScheme(), "https") ? 443 : 80) : url.getPort()) - .putHeaders(headers) - .ssl(Objects.equals(url.getScheme(), "https")) - .sendJson(httpConnectRecord.getData()) - .onSuccess(res -> { - log.info("Request sent successfully. Record: timestamp={}", timestamp); - - Exception e = null; - - // log the response - if (HttpUtils.is2xxSuccessful(res.statusCode())) { - if (log.isDebugEnabled()) { - log.debug("Received successful response: statusCode={}. Record: timestamp={}, responseBody={}", - res.statusCode(), timestamp, res.bodyAsString()); - } else { - log.info("Received successful response: statusCode={}. Record: timestamp={}", res.statusCode(), timestamp); - } - } else { - if (log.isDebugEnabled()) { - log.warn("Received non-2xx response: statusCode={}. Record: timestamp={}, responseBody={}", - res.statusCode(), timestamp, res.bodyAsString()); - } else { - log.warn("Received non-2xx response: statusCode={}. Record: timestamp={}", res.statusCode(), timestamp); - } - - e = new RuntimeException("Unexpected HTTP response code: " + res.statusCode()); - } - - // try callback - tryCallback(httpConnectRecord, e, attributes, connectRecord); - }).onFailure(err -> { - log.error("Request failed to send. Record: timestamp={}", timestamp, err); - - // try callback - tryCallback(httpConnectRecord, err, attributes, connectRecord); - }); - } - - /** - * Tries to call the callback based on the result of the request. - * - * @param httpConnectRecord the HttpConnectRecord to use - * @param e the exception thrown during the request, may be null - * @param attributes additional attributes to be used in processing - */ - private void tryCallback(HttpConnectRecord httpConnectRecord, Throwable e, Map attributes, ConnectRecord record) { - // get and update the attempt event - HttpAttemptEvent attemptEvent = (HttpAttemptEvent) attributes.get(HttpAttemptEvent.PREFIX + httpConnectRecord.getHttpRecordId()); - attemptEvent.updateEvent(e); - - // get and update the multiHttpRequestContext - MultiHttpRequestContext multiHttpRequestContext = getAndUpdateMultiHttpRequestContext(attributes, attemptEvent); - - if (multiHttpRequestContext.isAllRequestsProcessed()) { - // do callback - if (record.getCallback() == null) { - if (log.isDebugEnabled()) { - log.warn("ConnectRecord callback is null. Ignoring callback. {}", record); - } else { - log.warn("ConnectRecord callback is null. Ignoring callback."); - } - return; - } - - // get the last failed event - HttpAttemptEvent lastFailedEvent = multiHttpRequestContext.getLastFailedEvent(); - if (lastFailedEvent == null) { - // success - record.getCallback().onSuccess(convertToSendResult(record)); - } else { - // failure - record.getCallback().onException(buildSendExceptionContext(record, lastFailedEvent.getLastException())); - } - } else { - log.warn("still have requests to process, size {}|attempt num {}", - multiHttpRequestContext.getRemainingRequests(), attemptEvent.getAttempts()); - } - } - - - /** - * Gets and updates the multi http request context based on the provided attributes and HttpConnectRecord. - * - * @param attributes the attributes to use - * @param attemptEvent the HttpAttemptEvent to use - * @return the updated multi http request context - */ - private MultiHttpRequestContext getAndUpdateMultiHttpRequestContext(Map attributes, HttpAttemptEvent attemptEvent) { - // get the multi http request context - MultiHttpRequestContext multiHttpRequestContext = (MultiHttpRequestContext) attributes.get(MultiHttpRequestContext.NAME); - - // Check if the current attempted event has completed - if (attemptEvent.isComplete()) { - // decrement the counter - multiHttpRequestContext.decrementRemainingRequests(); - - if (attemptEvent.getLastException() != null) { - // if all attempts are exhausted, set the last failed event - multiHttpRequestContext.setLastFailedEvent(attemptEvent); - } - } - - return multiHttpRequestContext; - } - - private SendResult convertToSendResult(ConnectRecord record) { - SendResult result = new SendResult(); - result.setMessageId(record.getRecordId()); - if (org.apache.commons.lang3.StringUtils.isNotEmpty(record.getExtension("topic"))) { - result.setTopic(record.getExtension("topic")); - } - return result; - } - - private SendExceptionContext buildSendExceptionContext(ConnectRecord record, Throwable e) { - SendExceptionContext sendExceptionContext = new SendExceptionContext(); - sendExceptionContext.setMessageId(record.getRecordId()); - sendExceptionContext.setCause(e); - if (org.apache.commons.lang3.StringUtils.isNotEmpty(record.getExtension("topic"))) { - sendExceptionContext.setTopic(record.getExtension("topic")); - } - return sendExceptionContext; - } - - - /** - * Cleans up and releases resources used by the HTTP/HTTPS handler. - */ - @Override - public void stop() { - if (this.webClient != null) { - this.webClient.close(); - } else { - log.warn("WebClient is null, ignore."); - } - } -} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java deleted file mode 100644 index 6e8f43d6cb..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.handler.impl; - -import dev.failsafe.Failsafe; -import dev.failsafe.RetryPolicy; -import io.vertx.core.Future; -import io.vertx.core.buffer.Buffer; -import io.vertx.ext.web.client.HttpResponse; -import lombok.extern.slf4j.Slf4j; -import org.apache.eventmesh.common.config.connector.http.HttpRetryConfig; -import org.apache.eventmesh.common.config.connector.http.SinkConnectorConfig; -import org.apache.eventmesh.connector.http.sink.data.HttpConnectRecord; -import org.apache.eventmesh.connector.http.sink.handler.AbstractHttpSinkHandler; -import org.apache.eventmesh.connector.http.sink.handler.HttpSinkHandler; -import org.apache.eventmesh.connector.http.util.HttpUtils; -import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; - -import java.net.ConnectException; -import java.net.URI; -import java.time.Duration; -import java.util.Map; - - -/** - * HttpSinkHandlerRetryWrapper is a wrapper class for the HttpSinkHandler that provides retry functionality for failed HTTP requests. - */ -@Slf4j -public class McpSinkHandlerRetryWrapper extends AbstractHttpSinkHandler { - - private final HttpRetryConfig httpRetryConfig; - - private final HttpSinkHandler sinkHandler; - - private final RetryPolicy> retryPolicy; - - public McpSinkHandlerRetryWrapper(SinkConnectorConfig sinkConnectorConfig, HttpSinkHandler sinkHandler) { - super(sinkConnectorConfig); - this.sinkHandler = sinkHandler; - this.httpRetryConfig = getSinkConnectorConfig().getRetryConfig(); - this.retryPolicy = buildRetryPolicy(); - } - - private RetryPolicy> buildRetryPolicy() { - return RetryPolicy.>builder() - .handleIf(e -> e instanceof ConnectException) - .handleResultIf(response -> httpRetryConfig.isRetryOnNonSuccess() && !HttpUtils.is2xxSuccessful(response.statusCode())) - .withMaxRetries(httpRetryConfig.getMaxRetries()) - .withDelay(Duration.ofMillis(httpRetryConfig.getInterval())) - .onRetry(event -> { - if (log.isDebugEnabled()) { - log.warn("Failed to deliver message after {} attempts. Retrying in {} ms. Error: {}", - event.getAttemptCount(), httpRetryConfig.getInterval(), event.getLastException()); - } else { - log.warn("Failed to deliver message after {} attempts. Retrying in {} ms.", - event.getAttemptCount(), httpRetryConfig.getInterval()); - } - }).onFailure(event -> { - if (log.isDebugEnabled()) { - log.error("Failed to deliver message after {} attempts. Error: {}", - event.getAttemptCount(), event.getException()); - } else { - log.error("Failed to deliver message after {} attempts.", - event.getAttemptCount()); - } - }).build(); - } - - /** - * Initializes the WebClient for making HTTP requests based on the provided SinkConnectorConfig. - */ - @Override - public void start() { - sinkHandler.start(); - } - - - /** - * Processes HttpConnectRecord on specified URL while returning its own processing logic This method provides the retry power to process the - * HttpConnectRecord - * - * @param url URI to which the HttpConnectRecord should be sent - * @param httpConnectRecord HttpConnectRecord to process - * @param attributes additional attributes to pass to the processing chain - * @return processing chain - */ - @Override - public Future> deliver(URI url, HttpConnectRecord httpConnectRecord, Map attributes, - ConnectRecord connectRecord) { - Failsafe.with(retryPolicy) - .getStageAsync(() -> sinkHandler.deliver(url, httpConnectRecord, attributes, connectRecord).toCompletionStage()); - return null; - } - - - /** - * Cleans up and releases resources used by the HTTP/HTTPS handler. - */ - @Override - public void stop() { - sinkHandler.stop(); - } -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java index a0fe32d967..3ee6caaf69 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java @@ -30,6 +30,7 @@ import org.apache.eventmesh.common.config.connector.Config; import org.apache.eventmesh.common.config.connector.http.HttpSourceConfig; import org.apache.eventmesh.common.exception.EventMeshException; +import org.apache.eventmesh.connector.mcp.source.data.McpResponse; import org.apache.eventmesh.connector.mcp.source.protocol.Protocol; import org.apache.eventmesh.connector.mcp.source.protocol.ProtocolFactory; import org.apache.eventmesh.openconnect.api.ConnectorCreateService; @@ -123,9 +124,9 @@ public void start() { this.server.listen(res -> { if (res.succeeded()) { this.started = true; - log.info("HttpSourceConnector started on port: {}", this.sourceConfig.getConnectorConfig().getPort()); + log.info("McpSourceConnector started on port: {}", this.sourceConfig.getConnectorConfig().getPort()); } else { - log.error("HttpSourceConnector failed to start on port: {}", this.sourceConfig.getConnectorConfig().getPort()); + log.error("McpSourceConnector failed to start on port: {}", this.sourceConfig.getConnectorConfig().getPort()); throw new EventMeshException("failed to start Vertx server", res.cause()); } }); @@ -134,13 +135,13 @@ public void start() { @Override public void commit(ConnectRecord record) { if (sourceConfig.getConnectorConfig().isDataConsistencyEnabled()) { - log.debug("HttpSourceConnector commit record: {}", record.getRecordId()); + log.debug("McpSourceConnector commit record: {}", record.getRecordId()); RoutingContext routingContext = (RoutingContext) record.getExtensionObj("routingContext"); if (routingContext != null) { routingContext.response() .putHeader("content-type", "application/json") .setStatusCode(HttpResponseStatus.OK.code()) - .end(CommonResponse.success().toJsonStr()); + .end(McpResponse.success(null, "0").toJsonStr());// todo set session and outputs } else { log.error("Failed to commit the record, routingContext is null, recordId: {}", record.getRecordId()); } @@ -171,15 +172,15 @@ public void stop() { this.server.close(res -> { if (res.succeeded()) { this.destroyed = true; - log.info("HttpSourceConnector stopped on port: {}", this.sourceConfig.getConnectorConfig().getPort()); + log.info("McpSourceConnector stopped on port: {}", this.sourceConfig.getConnectorConfig().getPort()); } else { - log.error("HttpSourceConnector failed to stop on port: {}", this.sourceConfig.getConnectorConfig().getPort()); + log.error("McpSourceConnector failed to stop on port: {}", this.sourceConfig.getConnectorConfig().getPort()); throw new EventMeshException("failed to stop Vertx server", res.cause()); } } ); } else { - log.warn("HttpSourceConnector server is null, ignore."); + log.warn("McpSourceConnector server is null, ignore."); } } diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java index fafb02548a..68aac9d7b9 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java @@ -26,7 +26,7 @@ public class McpRequest implements Serializable { private Boolean stream; - private List inputs; + private Object inputs; private RoutingContext routingContext; } diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java index d495b06b5d..694de9a128 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java @@ -10,10 +10,12 @@ import org.apache.eventmesh.common.config.connector.http.SourceConnectorConfig; import org.apache.eventmesh.common.utils.JsonUtils; import org.apache.eventmesh.connector.mcp.source.data.McpRequest; +import org.apache.eventmesh.connector.mcp.source.data.McpResponse; import org.apache.eventmesh.connector.mcp.source.protocol.Protocol; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; import java.util.Base64; +import java.util.List; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.stream.Collectors; @@ -25,7 +27,7 @@ */ @Slf4j public class McpStandardProtocol implements Protocol { - public static final String PROTOCOL_NAME = "Common"; + public static final String PROTOCOL_NAME = "Mcp"; private SourceConnectorConfig sourceConnectorConfig; @@ -54,10 +56,10 @@ public void setHandler(Route route, BlockingQueue queue) { Object payload = ctx.body().asString(Constants.DEFAULT_CHARSET.toString()); payload = JsonUtils.parseObject(payload.toString(), String.class); - // Create and store the webhook request + // Create and store the mcp request Map headerMap = ctx.request().headers().entries().stream() .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - WebhookRequest webhookRequest = new WebhookRequest(PROTOCOL_NAME, ctx.request().absoluteURI(), headerMap, payload, ctx); + McpRequest webhookRequest = new McpRequest(PROTOCOL_NAME, ctx.request().absoluteURI(), headerMap, true, payload, ctx); if (!queue.offer(webhookRequest)) { throw new IllegalStateException("Failed to store the request."); } @@ -66,7 +68,7 @@ public void setHandler(Route route, BlockingQueue queue) { // Return 200 OK ctx.response() .setStatusCode(HttpResponseStatus.OK.code()) - .end(CommonResponse.success().toJsonStr()); + .end(McpResponse.success(null, "0").toJsonStr()); } }) @@ -76,7 +78,7 @@ public void setHandler(Route route, BlockingQueue queue) { // Return Bad Response ctx.response() .setStatusCode(ctx.statusCode()) - .end(CommonResponse.base(ctx.failure().getMessage()).toJsonStr()); + .end(McpResponse.base(null, "0", ctx.failure().getMessage()).toJsonStr()); // todo }); } @@ -90,10 +92,9 @@ public void setHandler(Route route, BlockingQueue queue) { @Override public ConnectRecord convertToConnectRecord(Object message) { McpRequest request = (McpRequest) message; - ConnectRecord connectRecord = new ConnectRecord(null, null, System.currentTimeMillis(), request.getPayload()); + ConnectRecord connectRecord = new ConnectRecord(null, null, System.currentTimeMillis(), request.getInputs()); connectRecord.addExtension("source", request.getProtocolName()); - connectRecord.addExtension("url", request.getUrl()); - request.getHeaders().forEach((k, v) -> { + request.getMetadata().forEach((k, v) -> { if (k.equalsIgnoreCase("extension")) { JsonObject extension = new JsonObject(v); extension.forEach(e -> connectRecord.addExtension(e.getKey(), e.getValue())); diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/source-config.yml b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/source-config.yml index 937b58f94e..eb8cc07d02 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/source-config.yml +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/source-config.yml @@ -26,7 +26,7 @@ pubSubConfig: passWord: httpPassWord connectorConfig: connectorName: mcpSource - path: /mcp/message + path: /test port: 7091 idleTimeout: 5000 # timeunit: ms maxFormAttributeSize: 1048576 # timeunit: byte, default: 1048576(1MB). This applies only when handling form data submissions. From 8d622c02cb7be669de480a1edcd83ece34d0c158 Mon Sep 17 00:00:00 2001 From: jerryyummy <461697582@qq.com> Date: Wed, 2 Jul 2025 23:11:59 +0800 Subject: [PATCH 05/36] build the basic mcp server without streamable http --- .../apache/eventmesh/connector/mcp/source/data/McpRequest.java | 1 - .../connector/mcp/source/protocol/impl/McpStandardProtocol.java | 1 - 2 files changed, 2 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java index 68aac9d7b9..1172992166 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java @@ -6,7 +6,6 @@ import lombok.NoArgsConstructor; import java.io.Serializable; -import java.util.List; import java.util.Map; /** diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java index 694de9a128..945c65e754 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java @@ -15,7 +15,6 @@ import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; import java.util.Base64; -import java.util.List; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.stream.Collectors; From c6802b0a26ca3c8222daf5a36a4a1513b867eb99 Mon Sep 17 00:00:00 2001 From: jerryyummy <461697582@qq.com> Date: Fri, 4 Jul 2025 01:05:53 +0800 Subject: [PATCH 06/36] build the basic mcp server without streamable http --- .../config/connector/mcp/HttpSinkConfig.java | 30 ------- ...SourceConfig.java => McpSourceConfig.java} | 3 +- .../connector/mcp/SinkConnectorConfig.java | 83 ------------------- .../connector/mcp/SourceConnectorConfig.java | 2 +- .../eventmesh/common/utils/JsonUtils.java | 11 +++ .../mcp/source/McpSourceConnector.java | 9 +- .../mcp/source/protocol/Protocol.java | 2 +- .../mcp/source/protocol/ProtocolFactory.java | 2 +- .../protocol/impl/McpStandardProtocol.java | 16 ++-- 9 files changed, 29 insertions(+), 129 deletions(-) delete mode 100644 eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpSinkConfig.java rename eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/{HttpSourceConfig.java => McpSourceConfig.java} (89%) delete mode 100644 eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SinkConnectorConfig.java diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpSinkConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpSinkConfig.java deleted file mode 100644 index 866c228a88..0000000000 --- a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpSinkConfig.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.common.config.connector.mcp; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.apache.eventmesh.common.config.connector.SinkConfig; -import org.apache.eventmesh.common.config.connector.http.SinkConnectorConfig; - -@Data -@EqualsAndHashCode(callSuper = true) -public class HttpSinkConfig extends SinkConfig { - - public SinkConnectorConfig connectorConfig; -} diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpSourceConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpSourceConfig.java similarity index 89% rename from eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpSourceConfig.java rename to eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpSourceConfig.java index 50ee8a2440..4cf6ae3875 100644 --- a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpSourceConfig.java +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpSourceConfig.java @@ -20,11 +20,10 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.apache.eventmesh.common.config.connector.SourceConfig; -import org.apache.eventmesh.common.config.connector.http.SourceConnectorConfig; @Data @EqualsAndHashCode(callSuper = true) -public class HttpSourceConfig extends SourceConfig { +public class McpSourceConfig extends SourceConfig { public SourceConnectorConfig connectorConfig; } diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SinkConnectorConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SinkConnectorConfig.java deleted file mode 100644 index 3cb56d9362..0000000000 --- a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SinkConnectorConfig.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.common.config.connector.mcp; - - -import lombok.Data; -import org.apache.eventmesh.common.config.connector.http.HttpRetryConfig; -import org.apache.eventmesh.common.config.connector.http.HttpWebhookConfig; - -@Data -public class SinkConnectorConfig { - - private String connectorName; - - private String[] urls; - - // keepAlive, default true - private boolean keepAlive = true; - - // timeunit: ms, default 60000ms - private int keepAliveTimeout = 60 * 1000; // Keep units consistent - - // timeunit: ms, default 5000ms, recommended scope: 5000ms - 10000ms - private int connectionTimeout = 5000; - - // timeunit: ms, default 5000ms - private int idleTimeout = 5000; - - // maximum number of HTTP/1 connections a client will pool, default 50 - private int maxConnectionPoolSize = 50; - - // retry config - private HttpRetryConfig retryConfig = new HttpRetryConfig(); - - // mcp config - private HttpMcpConfig mcpConfig = new HttpMcpConfig(); - - private String deliveryStrategy = "ROUND_ROBIN"; - - private boolean skipDeliverException = false; - - // managed pipelining param, default true - private boolean isParallelized = true; - - private int parallelism = 2; - - - /** - * Fill default values if absent (When there are multiple default values for a field) - * - * @param config SinkConnectorConfig - */ - public static void populateFieldsWithDefaults(SinkConnectorConfig config) { - /* - * set default values for idleTimeout - * recommended scope: common(5s - 10s), mcp(15s - 30s) - */ - final int commonHttpIdleTimeout = 5000; - final int mcpHttpIdleTimeout = 15000; - - // Set default values for idleTimeout - if (config.getIdleTimeout() == 0) { - int idleTimeout = config.mcpConfig.isActivate() ? mcpHttpIdleTimeout : commonHttpIdleTimeout; - config.setIdleTimeout(idleTimeout); - } - - } -} diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SourceConnectorConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SourceConnectorConfig.java index 98a7025b1a..03b6a48724 100644 --- a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SourceConnectorConfig.java +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SourceConnectorConfig.java @@ -27,7 +27,7 @@ public class SourceConnectorConfig { private String connectorName; - private String path = "/mcp"; + private String path = "/"; private int port; diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/utils/JsonUtils.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/utils/JsonUtils.java index f2328541c4..9b1bfe6f4e 100644 --- a/eventmesh-common/src/main/java/org/apache/eventmesh/common/utils/JsonUtils.java +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/utils/JsonUtils.java @@ -162,6 +162,17 @@ public static T parseObject(byte[] bytes, Class clazz) { } } + public static T parseObject(String text, TypeReference typeReference) { + if (StringUtils.isEmpty(text)) { + return null; + } + try { + return OBJECT_MAPPER.readValue(text, typeReference); + } catch (JsonProcessingException e) { + throw new JsonException("deserialize json string to object error", e); + } + } + /** * parse json string to object. * diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java index 3ee6caaf69..cfebfff1f1 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java @@ -29,6 +29,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.eventmesh.common.config.connector.Config; import org.apache.eventmesh.common.config.connector.http.HttpSourceConfig; +import org.apache.eventmesh.common.config.connector.mcp.McpSourceConfig; import org.apache.eventmesh.common.exception.EventMeshException; import org.apache.eventmesh.connector.mcp.source.data.McpResponse; import org.apache.eventmesh.connector.mcp.source.protocol.Protocol; @@ -48,7 +49,7 @@ @Slf4j public class McpSourceConnector implements Source, ConnectorCreateService { - private HttpSourceConfig sourceConfig; + private McpSourceConfig sourceConfig; private BlockingQueue queue; @@ -69,7 +70,7 @@ public class McpSourceConnector implements Source, ConnectorCreateService configClass() { - return HttpSourceConfig.class; + return McpSourceConfig.class; } @Override @@ -79,14 +80,14 @@ public Source create() { @Override public void init(Config config) { - this.sourceConfig = (HttpSourceConfig) config; + this.sourceConfig = (McpSourceConfig) config; doInit(); } @Override public void init(ConnectorContext connectorContext) { SourceConnectorContext sourceConnectorContext = (SourceConnectorContext) connectorContext; - this.sourceConfig = (HttpSourceConfig) sourceConnectorContext.getSourceConfig(); + this.sourceConfig = (McpSourceConfig) sourceConnectorContext.getSourceConfig(); doInit(); } diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java index d0ac2d06a8..71b9b7cb1f 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java @@ -18,7 +18,7 @@ package org.apache.eventmesh.connector.mcp.source.protocol; import io.vertx.ext.web.Route; -import org.apache.eventmesh.common.config.connector.http.SourceConnectorConfig; +import org.apache.eventmesh.common.config.connector.mcp.SourceConnectorConfig; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; import java.util.concurrent.BlockingQueue; diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/ProtocolFactory.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/ProtocolFactory.java index 15e4ca3e77..883992a85d 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/ProtocolFactory.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/ProtocolFactory.java @@ -1,6 +1,6 @@ package org.apache.eventmesh.connector.mcp.source.protocol; -import org.apache.eventmesh.common.config.connector.http.SourceConnectorConfig; +import org.apache.eventmesh.common.config.connector.mcp.SourceConnectorConfig; import org.apache.eventmesh.connector.mcp.source.protocol.impl.McpStandardProtocol; import java.util.Optional; diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java index 945c65e754..140395f843 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java @@ -1,5 +1,6 @@ package org.apache.eventmesh.connector.mcp.source.protocol.impl; +import com.fasterxml.jackson.core.type.TypeReference; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.http.HttpMethod; import io.vertx.core.json.JsonObject; @@ -7,7 +8,7 @@ import io.vertx.ext.web.handler.BodyHandler; import lombok.extern.slf4j.Slf4j; import org.apache.eventmesh.common.Constants; -import org.apache.eventmesh.common.config.connector.http.SourceConnectorConfig; +import org.apache.eventmesh.common.config.connector.mcp.SourceConnectorConfig; import org.apache.eventmesh.common.utils.JsonUtils; import org.apache.eventmesh.connector.mcp.source.data.McpRequest; import org.apache.eventmesh.connector.mcp.source.data.McpResponse; @@ -52,22 +53,23 @@ public void setHandler(Route route, BlockingQueue queue) { .handler(BodyHandler.create()) .handler(ctx -> { // Get the payload - Object payload = ctx.body().asString(Constants.DEFAULT_CHARSET.toString()); - payload = JsonUtils.parseObject(payload.toString(), String.class); + String jsonStr = ctx.body().asString(Constants.DEFAULT_CHARSET.toString()); + Map payloadMap = JsonUtils.parseObject(jsonStr, new TypeReference>() {}); // Create and store the mcp request Map headerMap = ctx.request().headers().entries().stream() .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - McpRequest webhookRequest = new McpRequest(PROTOCOL_NAME, ctx.request().absoluteURI(), headerMap, true, payload, ctx); - if (!queue.offer(webhookRequest)) { + McpRequest mcpRequest = new McpRequest(PROTOCOL_NAME, ctx.request().absoluteURI(), headerMap, true, payloadMap, ctx); + if (!queue.offer(mcpRequest)) { throw new IllegalStateException("Failed to store the request."); } - if (!sourceConnectorConfig.isDataConsistencyEnabled()) { + if (sourceConnectorConfig.isDataConsistencyEnabled()) { // Return 200 OK ctx.response() .setStatusCode(HttpResponseStatus.OK.code()) - .end(McpResponse.success(null, "0").toJsonStr()); + .end(McpResponse.success(payloadMap.get("inputs"), (String) payloadMap.get("session_id")).toJsonStr()); + //.end(McpResponse.success(null, "0").toJsonStr()); } }) From 684ccbd47534579a61139781798368b5014893e1 Mon Sep 17 00:00:00 2001 From: jerryyummy <461697582@qq.com> Date: Fri, 11 Jul 2025 14:13:30 +0800 Subject: [PATCH 07/36] build the basic mcp server with streamable http --- .../connector/mcp/session/MCPSession.java | 34 + .../connector/mcp/session/McpSession.java | 62 -- .../mcp/session/McpSessionManager.java | 58 -- .../connector/mcp/session/SessionManager.java | 72 ++ .../mcp/source/McpSourceConnector.java | 340 +++++++--- .../mcp/source/data/MCPStreamingResponse.java | 210 ++++++ .../connector/mcp/source/data/McpRequest.java | 48 +- .../mcp/source/data/McpStreamingRequest.java | 25 + .../handler/NettyMcpRequestHandler.java | 614 ++++++++++++++++++ .../mcp/source/protocol/Protocol.java | 148 ++++- .../protocol/impl/McpStandardProtocol.java | 499 +++++++++++--- .../connector/mcp/util/AIToolManager.java | 162 +++++ .../mcp/util/CloudEventsExtensionUtil.java | 213 ++++++ .../mcp/util/McpResponseValidator.java | 311 +++++++++ .../connector/mcp/util/McpUtils.java | 4 - 15 files changed, 2454 insertions(+), 346 deletions(-) create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/MCPSession.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSession.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSessionManager.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/SessionManager.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/MCPStreamingResponse.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpStreamingRequest.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/handler/NettyMcpRequestHandler.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/AIToolManager.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/CloudEventsExtensionUtil.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/McpResponseValidator.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/McpUtils.java diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/MCPSession.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/MCPSession.java new file mode 100644 index 0000000000..821e138249 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/MCPSession.java @@ -0,0 +1,34 @@ +package org.apache.eventmesh.connector.mcp.session; + +import lombok.Getter; + +import java.util.concurrent.ConcurrentHashMap; + +public class MCPSession { + @Getter + private final String sessionId; + private final ConcurrentHashMap context = new ConcurrentHashMap<>(); + @Getter + private volatile long lastActivity; + + public MCPSession(String sessionId) { + this.sessionId = sessionId; + this.lastActivity = System.currentTimeMillis(); + } + + public void updateLastActivity() { + this.lastActivity = System.currentTimeMillis(); + } + + public void setContext(String key, Object value) { + context.put(key, value); + } + + public Object getContext(String key) { + return context.get(key); + } + + public void clearContext() { + context.clear(); + } +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSession.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSession.java deleted file mode 100644 index 460fd1dcac..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSession.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.apache.eventmesh.connector.mcp.session; - -import io.netty.channel.Channel; - -import java.time.Instant; -import java.util.LinkedList; -import java.util.Queue; - -public class McpSession { - - private final String sessionId; - - // 与客户端保持通信的通道 - private Channel channel; - - // 最近活跃时间,用于超时清理 - private Instant lastActiveTime; - - // 如果是 Stream 模式,可以缓存未发出的数据块 - private final Queue streamBuffer = new LinkedList<>(); - - public McpSession(String sessionId, Channel channel) { - this.sessionId = sessionId; - this.channel = channel; - this.lastActiveTime = Instant.now(); - } - - public String getSessionId() { - return sessionId; - } - - public Channel getChannel() { - return channel; - } - - public void setChannel(Channel channel) { - this.channel = channel; - touch(); - } - - public void addToBuffer(Object data) { - streamBuffer.add(data); - } - - public Queue getStreamBuffer() { - return streamBuffer; - } - - public Instant getLastActiveTime() { - return lastActiveTime; - } - - public void touch() { - this.lastActiveTime = Instant.now(); - } - - public void close() { - if (channel != null && channel.isOpen()) { - channel.close(); - } - } -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSessionManager.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSessionManager.java deleted file mode 100644 index 5163d822de..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/McpSessionManager.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.apache.eventmesh.connector.mcp.session; - -import java.time.Duration; -import java.time.Instant; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -public class McpSessionManager { - - private static final long SESSION_TIMEOUT_SECONDS = 300; - - private static final McpSessionManager INSTANCE = new McpSessionManager(); - - private final Map sessionMap = new ConcurrentHashMap<>(); - - private McpSessionManager() { - } - - public static McpSessionManager getInstance() { - return INSTANCE; - } - - public McpSession getOrCreateSession(String sessionId, io.netty.channel.Channel channel) { - return sessionMap.compute(sessionId, (id, existing) -> { - if (existing != null) { - existing.setChannel(channel); // 恢复连接 - return existing; - } else { - return new McpSession(sessionId, channel); - } - }); - } - - public McpSession getSession(String sessionId) { - return sessionMap.get(sessionId); - } - - public void closeSession(String sessionId) { - McpSession session = sessionMap.remove(sessionId); - if (session != null) { - session.close(); - } - } - - public void clearTimeoutSessions() { - Instant now = Instant.now(); - for (Map.Entry entry : sessionMap.entrySet()) { - if (Duration.between(entry.getValue().getLastActiveTime(), now).getSeconds() > SESSION_TIMEOUT_SECONDS) { - closeSession(entry.getKey()); - } - } - } - - public int activeSessionCount() { - return sessionMap.size(); - } -} - diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/SessionManager.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/SessionManager.java new file mode 100644 index 0000000000..a34da41f49 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/SessionManager.java @@ -0,0 +1,72 @@ +package org.apache.eventmesh.connector.mcp.session; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class SessionManager { + private final ConcurrentHashMap sessions = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + + public SessionManager() { + // 定时清理过期会话 + scheduler.scheduleAtFixedRate(this::cleanupExpiredSessions, 5, 5, TimeUnit.MINUTES); + } + + public MCPSession createSession() { + String sessionId = UUID.randomUUID().toString(); + MCPSession session = new MCPSession(sessionId); + sessions.put(sessionId, session); + return session; + } + + public MCPSession createSessionWithId(String sessionId) { + if (sessionId == null || sessionId.trim().isEmpty()) { + return createSession(); + } + + // 检查会话ID是否已存在 + if (sessions.containsKey(sessionId)) { + return sessions.get(sessionId); + } + + MCPSession session = new MCPSession(sessionId); + sessions.put(sessionId, session); + return session; + } + + public void putSession(MCPSession session) { + if (session != null) { + sessions.put(session.getSessionId(), session); + } + } + + public MCPSession getSession(String sessionId) { + MCPSession session = sessions.get(sessionId); + if (session != null) { + session.updateLastActivity(); + } + return session; + } + + public void removeSession(String sessionId) { + sessions.remove(sessionId); + } + + public void cleanupExpiredSessions(long currentTime) { + sessions.entrySet().removeIf(entry -> { + MCPSession session = entry.getValue(); + return currentTime - session.getLastActivity() > 30 * 60 * 1000; // 30分钟超时 + }); + } + + private void cleanupExpiredSessions() { + cleanupExpiredSessions(System.currentTimeMillis()); + } + + public void shutdown() { + scheduler.shutdown(); + } +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java index cfebfff1f1..5e85ac1078 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java @@ -1,37 +1,19 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.source; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.ext.web.Route; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.LoggerHandler; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.stream.ChunkedWriteHandler; + import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.eventmesh.common.config.connector.Config; -import org.apache.eventmesh.common.config.connector.http.HttpSourceConfig; import org.apache.eventmesh.common.config.connector.mcp.McpSourceConfig; import org.apache.eventmesh.common.exception.EventMeshException; -import org.apache.eventmesh.connector.mcp.source.data.McpResponse; import org.apache.eventmesh.connector.mcp.source.protocol.Protocol; import org.apache.eventmesh.connector.mcp.source.protocol.ProtocolFactory; import org.apache.eventmesh.openconnect.api.ConnectorCreateService; @@ -40,26 +22,39 @@ import org.apache.eventmesh.openconnect.api.source.Source; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +/** + * MCP Source Connector (Netty Implementation) + * + * 职责: + * 1. 连接器生命周期管理(启动、停止) + * 2. Netty服务器的创建和管理 + * 3. 请求队列的管理 + * 4. 数据轮询和批处理 + * 5. 连接器配置管理 + * 6. 错误处理和监控 + */ @Slf4j public class McpSourceConnector implements Source, ConnectorCreateService { private McpSourceConfig sourceConfig; - + // Getter methods for testing and monitoring + @Getter private BlockingQueue queue; - private int batchSize; - - private Route route; - + @Getter private Protocol protocol; - private HttpServer server; + // Netty 服务器组件 + @Getter + private EventLoopGroup bossGroup; + @Getter + private EventLoopGroup workerGroup; + private Channel serverChannel; @Getter private volatile boolean started = false; @@ -67,7 +62,6 @@ public class McpSourceConnector implements Source, ConnectorCreateService configClass() { return McpSourceConfig.class; @@ -91,60 +85,144 @@ public void init(ConnectorContext connectorContext) { doInit(); } + /** + * 初始化连接器 + */ private void doInit() { - // init queue + // 初始化队列 int maxQueueSize = this.sourceConfig.getConnectorConfig().getMaxStorageSize(); this.queue = new LinkedBlockingQueue<>(maxQueueSize); - // init batch size + // 初始化批处理大小 this.batchSize = this.sourceConfig.getConnectorConfig().getBatchSize(); - // init protocol + // 初始化协议处理器 String protocolName = this.sourceConfig.getConnectorConfig().getProtocol(); this.protocol = ProtocolFactory.getInstance(this.sourceConfig.connectorConfig, protocolName); - final Vertx vertx = Vertx.vertx(); - final Router router = Router.router(vertx); - route = router.route() - .path(this.sourceConfig.connectorConfig.getPath()) - .handler(LoggerHandler.create()); + // 初始化协议处理器(传入队列引用) + this.protocol.initialize(this.sourceConfig.connectorConfig, this.queue); - // set protocol handler - this.protocol.setHandler(route, queue); + // 初始化 Netty 事件循环组 + this.bossGroup = new NioEventLoopGroup(1); + this.workerGroup = new NioEventLoopGroup(); - // create server - this.server = vertx.createHttpServer(new HttpServerOptions() - .setPort(this.sourceConfig.connectorConfig.getPort()) - .setMaxFormAttributeSize(this.sourceConfig.connectorConfig.getMaxFormAttributeSize()) - .setIdleTimeout(this.sourceConfig.connectorConfig.getIdleTimeout()) - .setIdleTimeoutUnit(TimeUnit.MILLISECONDS)).requestHandler(router); + log.info("McpSourceConnector initialized with protocol: {}, maxQueueSize: {}, batchSize: {}", + protocolName, maxQueueSize, batchSize); } @Override public void start() { - this.server.listen(res -> { - if (res.succeeded()) { - this.started = true; - log.info("McpSourceConnector started on port: {}", this.sourceConfig.getConnectorConfig().getPort()); - } else { - log.error("McpSourceConnector failed to start on port: {}", this.sourceConfig.getConnectorConfig().getPort()); - throw new EventMeshException("failed to start Vertx server", res.cause()); + try { + // 创建Netty服务器 + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, 1024) + .option(ChannelOption.SO_REUSEADDR, true) + .childOption(ChannelOption.SO_KEEPALIVE, true) + .childOption(ChannelOption.TCP_NODELAY, true) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + + // HTTP 编解码器 + pipeline.addLast("httpCodec", new HttpServerCodec()); + + // HTTP 对象聚合器 + pipeline.addLast("httpAggregator", new HttpObjectAggregator( + sourceConfig.getConnectorConfig().getMaxFormAttributeSize())); + + // 支持分块传输 + pipeline.addLast("chunkedWriter", new ChunkedWriteHandler()); + + // 协议处理器 + pipeline.addLast("protocolHandler", protocol.createHandler(sourceConfig)); + } + }); + + // 绑定端口并启动服务器 + int port = sourceConfig.getConnectorConfig().getPort(); + ChannelFuture future = bootstrap.bind(port).sync(); + this.serverChannel = future.channel(); + this.started = true; + + log.info("Netty McpSourceConnector started successfully on port: {}", port); + + // 启动后台任务 + startBackgroundTasks(); + + } catch (Exception e) { + log.error("Failed to start Netty McpSourceConnector", e); + throw new EventMeshException("Failed to start Netty server", e); + } + } + + /** + * 启动后台任务 + */ + private void startBackgroundTasks() { + // 启动队列监控任务 + workerGroup.scheduleAtFixedRate(() -> { + try { + monitorQueue(); + } catch (Exception e) { + log.error("Error in queue monitoring", e); + } + }, 10, 30, TimeUnit.SECONDS); + + // 启动健康检查任务 + workerGroup.scheduleAtFixedRate(() -> { + try { + performHealthCheck(); + } catch (Exception e) { + log.error("Error in health check", e); } - }); + }, 5, 60, TimeUnit.SECONDS); + } + + /** + * 监控队列状态 + */ + private void monitorQueue() { + int queueSize = queue.size(); + int maxQueueSize = sourceConfig.getConnectorConfig().getMaxStorageSize(); + + if (queueSize > maxQueueSize * 0.8) { + log.warn("Queue usage is high: {}/{} ({}%)", queueSize, maxQueueSize, + (queueSize * 100 / maxQueueSize)); + } + + if (queueSize > 0) { + log.debug("Queue status: {}/{} items", queueSize, maxQueueSize); + } + } + + /** + * 执行健康检查 + */ + private void performHealthCheck() { + boolean isHealthy = serverChannel != null && serverChannel.isActive() && started && !destroyed; + + if (!isHealthy) { + log.warn("Health check failed: serverChannel.isActive={}, started={}, destroyed={}", + serverChannel != null ? serverChannel.isActive() : "null", started, destroyed); + } else { + log.debug("Health check passed"); + } } @Override public void commit(ConnectRecord record) { if (sourceConfig.getConnectorConfig().isDataConsistencyEnabled()) { - log.debug("McpSourceConnector commit record: {}", record.getRecordId()); - RoutingContext routingContext = (RoutingContext) record.getExtensionObj("routingContext"); - if (routingContext != null) { - routingContext.response() - .putHeader("content-type", "application/json") - .setStatusCode(HttpResponseStatus.OK.code()) - .end(McpResponse.success(null, "0").toJsonStr());// todo set session and outputs - } else { - log.error("Failed to commit the record, routingContext is null, recordId: {}", record.getRecordId()); + log.debug("Committing record: {}", record.getRecordId()); + + // 委托给协议处理器进行提交 + try { + protocol.commit(record); + } catch (Exception e) { + log.error("Failed to commit record: {}", record.getRecordId(), e); } } } @@ -156,32 +234,47 @@ public String name() { @Override public void onException(ConnectRecord record) { - if (this.route != null) { - this.route.failureHandler(ctx -> { - log.error("Failed to handle the request, recordId {}. ", record.getRecordId(), ctx.failure()); - // Return Bad Response - ctx.response() - .setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()) - .end("{\"status\":\"failed\",\"recordId\":\"" + record.getRecordId() + "\"}"); - }); + log.error("Exception occurred for record: {}", record.getRecordId()); + + // 委托给协议处理器处理异常 + try { + protocol.onException(record); + } catch (Exception e) { + log.error("Failed to handle exception for record: {}", record.getRecordId(), e); } } @Override public void stop() { - if (this.server != null) { - this.server.close(res -> { - if (res.succeeded()) { - this.destroyed = true; - log.info("McpSourceConnector stopped on port: {}", this.sourceConfig.getConnectorConfig().getPort()); - } else { - log.error("McpSourceConnector failed to stop on port: {}", this.sourceConfig.getConnectorConfig().getPort()); - throw new EventMeshException("failed to stop Vertx server", res.cause()); - } - } - ); - } else { - log.warn("McpSourceConnector server is null, ignore."); + log.info("Stopping McpSourceConnector..."); + + try { + // 停止协议处理器 + if (protocol != null) { + protocol.shutdown(); + } + + // 关闭服务器通道 + if (serverChannel != null) { + serverChannel.close().sync(); + } + + // 优雅关闭事件循环组 + if (bossGroup != null) { + bossGroup.shutdownGracefully(2, 10, TimeUnit.SECONDS).sync(); + } + if (workerGroup != null) { + workerGroup.shutdownGracefully(2, 10, TimeUnit.SECONDS).sync(); + } + + this.destroyed = true; + this.started = false; + + log.info("McpSourceConnector stopped successfully"); + + } catch (Exception e) { + log.error("Failed to stop McpSourceConnector gracefully", e); + throw new EventMeshException("Failed to stop Netty server", e); } } @@ -191,28 +284,73 @@ public List poll() { long maxPollWaitTime = 5000; long remainingTime = maxPollWaitTime; - // poll from queue List connectRecords = new ArrayList<>(batchSize); - for (int i = 0; i < batchSize; i++) { - try { + + try { + for (int i = 0; i < batchSize; i++) { Object obj = queue.poll(remainingTime, TimeUnit.MILLISECONDS); if (obj == null) { break; } - // convert to ConnectRecord + + // 委托给协议处理器转换记录 ConnectRecord connectRecord = protocol.convertToConnectRecord(obj); - connectRecords.add(connectRecord); + if (connectRecord != null) { + connectRecords.add(connectRecord); + } - // calculate elapsed time and update remaining time for next poll + // 计算剩余时间 long elapsedTime = System.currentTimeMillis() - startTime; - remainingTime = maxPollWaitTime > elapsedTime ? maxPollWaitTime - elapsedTime : 0; - } catch (Exception e) { - log.error("Failed to poll from queue.", e); - throw new RuntimeException(e); + remainingTime = Math.max(0, maxPollWaitTime - elapsedTime); + } + + if (!connectRecords.isEmpty()) { + log.debug("Polled {} records from queue", connectRecords.size()); } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("Polling interrupted", e); + } catch (Exception e) { + log.error("Error during polling", e); } + return connectRecords; } -} + /** + * 获取队列统计信息 + */ + public Map getQueueStats() { + Map stats = new HashMap<>(); + stats.put("queueSize", queue.size()); + stats.put("maxQueueSize", sourceConfig.getConnectorConfig().getMaxStorageSize()); + stats.put("batchSize", batchSize); + stats.put("queueUtilization", (double) queue.size() / sourceConfig.getConnectorConfig().getMaxStorageSize()); + return stats; + } + + /** + * 获取服务器状态 + */ + public Map getServerStatus() { + Map status = new HashMap<>(); + status.put("started", started); + status.put("destroyed", destroyed); + status.put("serverActive", serverChannel != null && serverChannel.isActive()); + status.put("port", sourceConfig.getConnectorConfig().getPort()); + status.put("connectorName", name()); + return status; + } + + /** + * 强制清空队列 + */ + public int clearQueue() { + int clearedCount = queue.size(); + queue.clear(); + log.info("Cleared {} items from queue", clearedCount); + return clearedCount; + } + +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/MCPStreamingResponse.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/MCPStreamingResponse.java new file mode 100644 index 0000000000..478d93e517 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/MCPStreamingResponse.java @@ -0,0 +1,210 @@ +package org.apache.eventmesh.connector.mcp.source.data; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; + +/** + * 流式结果类 + * 用于管理流式传输的数据块 + */ +@Slf4j +public class MCPStreamingResponse implements Serializable { + private final String[] chunks; + @Getter + private int currentIndex = 0; + @Getter + private final String originalContent; + + /** + * 构造函数 + * + * @param content 原始内容,将被分割成块 + */ + public MCPStreamingResponse(String content) { + this.originalContent = content; + this.chunks = splitContent(content); + log.debug("Created StreamingResult with {} chunks", chunks.length); + } + + /** + * 构造函数,支持自定义分割 + * + * @param content 原始内容 + * @param delimiter 分割符 + */ + public MCPStreamingResponse(String content, String delimiter) { + this.originalContent = content; + this.chunks = content.split(delimiter); + log.debug("Created StreamingResult with {} chunks using delimiter: {}", chunks.length, delimiter); + } + + /** + * 获取下一个数据块 + * + * @return 下一个数据块,如果没有更多数据则返回null + */ + public String getNextChunk() { + if (currentIndex < chunks.length) { + String chunk = chunks[currentIndex]; + currentIndex++; + + log.debug("Returning chunk {}/{}: {}", currentIndex, chunks.length, + chunk.length() > 50 ? chunk.substring(0, 50) + "..." : chunk); + + return chunk + " "; // 添加空格以保持词与词之间的间距 + } + + log.debug("No more chunks available"); + return null; + } + + /** + * 检查是否还有更多数据块 + * + * @return 如果还有数据块返回true,否则返回false + */ + public boolean hasMore() { + return currentIndex < chunks.length; + } + + /** + * 检查是否已完成 + * + * @return 如果所有数据块都已发送返回true,否则返回false + */ + public boolean isComplete() { + return currentIndex >= chunks.length; + } + + /** + * 获取总块数 + * + * @return 总块数 + */ + public int getTotalChunks() { + return chunks.length; + } + + /** + * 获取进度百分比 + * + * @return 进度百分比(0-100) + */ + public int getProgress() { + if (chunks.length == 0) { + return 100; + } + return (currentIndex * 100) / chunks.length; + } + + /** + * 重置到开始位置 + */ + public void reset() { + this.currentIndex = 0; + log.debug("StreamingResult reset to beginning"); + } + + /** + * 跳到指定位置 + * + * @param index 目标索引 + */ + public void seekTo(int index) { + if (index >= 0 && index <= chunks.length) { + this.currentIndex = index; + log.debug("StreamingResult seeked to index: {}", index); + } else { + log.warn("Invalid seek index: {}, valid range: 0-{}", index, chunks.length); + } + } + + /** + * 获取剩余块数 + * + * @return 剩余块数 + */ + public int getRemainingChunks() { + return Math.max(0, chunks.length - currentIndex); + } + + /** + * 预览下一个块(不移动指针) + * + * @return 下一个块的内容,如果没有则返回null + */ + public String peekNext() { + if (currentIndex < chunks.length) { + return chunks[currentIndex] + " "; + } + return null; + } + + /** + * 获取所有剩余的块 + * + * @return 剩余块的数组 + */ + public String[] getRemainingChunksArray() { + if (currentIndex >= chunks.length) { + return new String[0]; + } + + String[] remaining = new String[chunks.length - currentIndex]; + System.arraycopy(chunks, currentIndex, remaining, 0, remaining.length); + return remaining; + } + + /** + * 分割内容的私有方法 + * + * @param content 要分割的内容 + * @return 分割后的数组 + */ + private String[] splitContent(String content) { + if (content == null || content.trim().isEmpty()) { + return new String[]{"Empty content"}; + } + + // 优先按句号分割,保持句子完整性 + String[] sentences = content.split("。"); + if (sentences.length > 1) { + // 为非最后一个句子添加句号 + for (int i = 0; i < sentences.length - 1; i++) { + if (!sentences[i].trim().isEmpty()) { + sentences[i] = sentences[i].trim() + "。"; + } + } + // 过滤空字符串 + return java.util.Arrays.stream(sentences) + .filter(s -> !s.trim().isEmpty()) + .toArray(String[]::new); + } + + // 如果没有句号,按逗号分割 + String[] phrases = content.split(",|,"); + if (phrases.length > 1) { + return java.util.Arrays.stream(phrases) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .toArray(String[]::new); + } + + // 如果没有逗号,按空格分割词语 + String[] words = content.trim().split("\\s+"); + if (words.length > 5) { // 如果词语太多,按词分割 + return words; + } + + // 如果词语较少,保持原内容 + return new String[]{content.trim()}; + } + + @Override + public String toString() { + return String.format("StreamingResult{totalChunks=%d, currentIndex=%d, progress=%d%%, complete=%s}", + chunks.length, currentIndex, getProgress(), isComplete()); + } +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java index 1172992166..107f369768 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java @@ -4,6 +4,7 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.apache.eventmesh.connector.mcp.session.MCPSession; import java.io.Serializable; import java.util.Map; @@ -17,15 +18,42 @@ public class McpRequest implements Serializable { private static final long serialVersionUID = -483500600756490500L; - private String protocolName; - + private String protocol; + private String absoluteURI; + private Map headerMap; + private boolean isStreaming; + private Map payloadMap; + private RoutingContext ctx; private String sessionId; - - private Map metadata; - - private Boolean stream; - - private Object inputs; - - private RoutingContext routingContext; + private String protocolVersion; + private MCPSession session; + + // 构造器,为了兼容原有代码 + public McpRequest(String protocol, String absoluteURI, Map headerMap, + boolean isStreaming, Map payloadMap, RoutingContext ctx) { + this.protocol = protocol; + this.absoluteURI = absoluteURI; + this.headerMap = headerMap; + this.isStreaming = isStreaming; + this.payloadMap = payloadMap; + this.ctx = ctx; + } + + // 为了兼容原有代码,保留这些方法 + public String getProtocolName() { + return protocol; + } + + public Object getInputs() { + return payloadMap != null ? payloadMap.get("inputs") : null; + } + + public Map getMetadata() { + return payloadMap != null ? (Map) payloadMap.get("metadata") : null; + } + + public RoutingContext getRoutingContext() { + return ctx; + } } + diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpStreamingRequest.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpStreamingRequest.java new file mode 100644 index 0000000000..85cb524124 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpStreamingRequest.java @@ -0,0 +1,25 @@ +package org.apache.eventmesh.connector.mcp.source.data; + +import io.vertx.ext.web.RoutingContext; +import lombok.Data; +import org.apache.eventmesh.connector.mcp.session.MCPSession; + +import java.util.Map; /** + * Mcp Streaming Request + */ +@Data +public class McpStreamingRequest extends McpRequest { + + public McpStreamingRequest(String protocol, String absoluteURI, Map headerMap, + boolean isStreaming, Map payloadMap, + RoutingContext ctx, MCPSession session) { + super(protocol, absoluteURI, headerMap, isStreaming, payloadMap, ctx); + setSession(session); + } + + public McpStreamingRequest(String protocol, String absoluteURI, Map headerMap, + boolean isStreaming, Map payloadMap, + RoutingContext ctx, String sessionId, String protocolVersion, MCPSession session) { + super(protocol, absoluteURI, headerMap, isStreaming, payloadMap, ctx, sessionId, protocolVersion, session); + } +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/handler/NettyMcpRequestHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/handler/NettyMcpRequestHandler.java new file mode 100644 index 0000000000..53c2b95ce0 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/handler/NettyMcpRequestHandler.java @@ -0,0 +1,614 @@ +package org.apache.eventmesh.connector.mcp.source.handler; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.*; + import io.netty.util.CharsetUtil; + +import lombok.extern.slf4j.Slf4j; +import org.apache.eventmesh.common.config.connector.mcp.McpSourceConfig; +import org.apache.eventmesh.connector.mcp.source.data.McpRequest; +import org.apache.eventmesh.connector.mcp.source.data.McpStreamingRequest; +import org.apache.eventmesh.connector.mcp.util.AIToolManager; +import org.apache.eventmesh.connector.mcp.source.data.MCPStreamingResponse; +import org.apache.eventmesh.connector.mcp.session.MCPSession; +import org.apache.eventmesh.connector.mcp.session.SessionManager; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.eventmesh.connector.mcp.util.McpResponseValidator; + +import java.util.*; + import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * Netty MCP请求处理器 + */ +@Slf4j +public class NettyMcpRequestHandler extends SimpleChannelInboundHandler { + + private static final String PROTOCOL_NAME = "Mcp"; + private final McpSourceConfig sourceConfig; + private final BlockingQueue queue; + private final SessionManager sessionManager; + private final ObjectMapper objectMapper; + private final AIToolManager toolManager; + + public NettyMcpRequestHandler(McpSourceConfig sourceConfig, BlockingQueue queue, + SessionManager sessionManager, ObjectMapper objectMapper, + AIToolManager toolManager) { + this.sourceConfig = sourceConfig; + this.queue = queue; + this.sessionManager = sessionManager; + this.objectMapper = objectMapper; + this.toolManager = toolManager; + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { + // 处理CORS预检请求 + if (request.method() == HttpMethod.OPTIONS) { + sendCorsResponse(ctx); + return; + } + + // 处理健康检查GET请求 + if (request.method() == HttpMethod.GET) { + // 检查是否是SSE请求 + String accept = request.headers().get("Accept"); + if (accept != null && accept.contains("text/event-stream")) { + handleSSERequest(ctx, request); + return; + } else { + sendHealthCheckResponse(ctx); + return; + } + } + + // 检查路径是否匹配 + String targetPath = sourceConfig.getConnectorConfig().getPath(); + if (!targetPath.equals(request.uri())) { + sendNotFound(ctx); + return; + } + + // 只处理POST请求用于MCP调用 + if (request.method() != HttpMethod.POST) { + sendMethodNotAllowed(ctx); + return; + } + + try { + // 解析请求体 + String jsonStr = request.content().toString(CharsetUtil.UTF_8); + Map payloadMap = objectMapper.readValue(jsonStr, new TypeReference>() {}); + + // 从请求头获取MCP相关信息 + String mcpProtocolVersion = request.headers().get("Mcp-Protocol-Version"); + String mcpSessionId = request.headers().get("Mcp-Session-Id"); + + log.info("Received MCP request - Protocol: {}, Session: {}, URI: {}", + mcpProtocolVersion, mcpSessionId, request.uri()); + + // 检查是否是流式请求 + Boolean isStreaming = (Boolean) payloadMap.get("stream"); + if (Boolean.TRUE.equals(isStreaming)) { + handleStreamingRequest(ctx, request, payloadMap, mcpSessionId, mcpProtocolVersion); + } else { + handleNormalRequest(ctx, request, payloadMap, mcpSessionId, mcpProtocolVersion); + } + + } catch (Exception e) { + log.error("Error processing MCP request", e); + sendError(ctx, "Invalid request format: " + e.getMessage()); + } + } + + /** + * 处理普通MCP请求 + */ + private void handleNormalRequest(ChannelHandlerContext ctx, FullHttpRequest request, + Map payloadMap, String sessionId, String protocolVersion) { + + // 获取或创建会话 + MCPSession session = getOrCreateSession(sessionId); + session.updateLastActivity(); + + // 解析MCP请求结构 + List> inputs = (List>) payloadMap.get("inputs"); + Map context = (Map) payloadMap.get("context"); + Map metadata = (Map) payloadMap.get("metadata"); + + // 创建MCP请求对象 + Map headerMap = new HashMap<>(); + for (Map.Entry entry : request.headers().entries()) { + headerMap.put(entry.getKey(), entry.getValue()); + } + + McpRequest mcpRequest = new McpRequest( + PROTOCOL_NAME, + request.uri(), + headerMap, + false, + payloadMap, + null, // Netty没有RoutingContext + sessionId, + protocolVersion, + session + ); + + // 添加到队列 + if (!queue.offer(mcpRequest)) { + sendError(ctx, "Queue full, failed to process request"); + return; + } + + // 如果启用了数据一致性,处理请求并返回响应 + if (sourceConfig.getConnectorConfig().isDataConsistencyEnabled()) { + processNormalMcpRequest(ctx, inputs, context, metadata, session, sessionId, protocolVersion); + } + } + + /** + * 处理流式MCP请求 + */ + private void handleStreamingRequest(ChannelHandlerContext ctx, FullHttpRequest request, + Map payloadMap, String sessionId, String protocolVersion) { + + // 获取或创建会话 + MCPSession session = getOrCreateSession(sessionId); + session.updateLastActivity(); + + // 解析MCP请求结构 + List> inputs = (List>) payloadMap.get("inputs"); + Map context = (Map) payloadMap.get("context"); + Map metadata = (Map) payloadMap.get("metadata"); + + // 设置流式响应头 + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); + response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS"); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, Authorization, Mcp-Protocol-Version, Mcp-Session-Id"); + + if (protocolVersion != null) { + response.headers().set("Mcp-Protocol-Version", protocolVersion); + } + if (sessionId != null) { + response.headers().set("Mcp-Session-Id", sessionId); + } + + // 发送响应头 + ctx.write(response); + + // 创建流式请求对象 + Map headerMap = new HashMap<>(); + for (Map.Entry entry : request.headers().entries()) { + headerMap.put(entry.getKey(), entry.getValue()); + } + + McpStreamingRequest streamingRequest = new McpStreamingRequest( + PROTOCOL_NAME, + request.uri(), + headerMap, + true, + payloadMap, + null, // Netty没有RoutingContext + sessionId, + protocolVersion, + session + ); + + // 添加到队列 + if (!queue.offer(streamingRequest)) { + sendStreamingError(ctx, sessionId, "Queue full", protocolVersion); + return; + } + + // 异步处理流式响应 + processStreamingMcpRequest(ctx, inputs, context, metadata, session, sessionId, protocolVersion); + } + + /** + * 处理普通MCP请求 + */ + private void processNormalMcpRequest(ChannelHandlerContext ctx, List> inputs, + Map context, Map metadata, + MCPSession session, String sessionId, String protocolVersion) { + + // 提取用户输入 + String userContent = extractUserContent(inputs); + + // 设置会话上下文 + if (context != null) { + session.setContext("history", context.get("history")); + session.setContext("context", context); + } + + // 异步处理请求 + toolManager.processRequest(userContent, session, metadata) + .thenAccept(result -> { + try { + // 构造MCP响应 + Map responseMap = new HashMap<>(); + responseMap.put("session_id", sessionId); + + Map output = new HashMap<>(); + output.put("role", "assistant"); + output.put("content", result); + responseMap.put("outputs", Arrays.asList(output)); + + Map metadataMap = new HashMap<>(); + metadataMap.put("timestamp", System.currentTimeMillis()); + metadataMap.put("version", protocolVersion != null ? protocolVersion : "2025-03-26"); + metadataMap.put("streaming", false); + responseMap.put("metadata", metadataMap); + + String responseJson = objectMapper.writeValueAsString(responseMap); + + // 发送响应 + ByteBuf content = Unpooled.copiedBuffer(responseJson, CharsetUtil.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); + response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + + if (protocolVersion != null) { + response.headers().set("Mcp-Protocol-Version", protocolVersion); + } + if (sessionId != null) { + response.headers().set("Mcp-Session-Id", sessionId); + } + + ctx.writeAndFlush(response); + + } catch (Exception e) { + log.error("Error processing response", e); + sendError(ctx, "Error processing response: " + e.getMessage()); + } + }) + .exceptionally(throwable -> { + log.error("Error processing request", throwable); + sendError(ctx, "Error processing request: " + throwable.getMessage()); + return null; + }); + } + + /** + * 处理流式MCP请求 + */ + private void processStreamingMcpRequest(ChannelHandlerContext ctx, List> inputs, + Map context, Map metadata, + MCPSession session, String sessionId, String protocolVersion) { + + // 提取用户输入 + String userContent = extractUserContent(inputs); + + // 设置会话上下文 + if (context != null) { + session.setContext("history", context.get("history")); + session.setContext("context", context); + } + + // 异步处理流式请求 + toolManager.processStreamingRequest(userContent, session, metadata) + .thenAccept(streamingResult -> { + startMcpStreaming(ctx, sessionId, streamingResult, protocolVersion); + }) + .exceptionally(throwable -> { + log.error("Error processing streaming request", throwable); + sendStreamingError(ctx, sessionId, throwable.getMessage(), protocolVersion); + return null; + }); + } + + /** + * 开始MCP流式传输 + */ + private void startMcpStreaming(ChannelHandlerContext ctx, String sessionId, + MCPStreamingResponse result, String protocolVersion) { + + // 使用Netty的事件循环定时器实现流式传输 + ctx.executor().scheduleAtFixedRate(() -> { + if (!ctx.channel().isActive()) { + return; + } + + try { + // 获取下一个数据块 + String chunk = result.getNextChunk(); + if (chunk != null) { + // 使用验证器创建标准的MCP响应 + String responseJson = McpResponseValidator.createStreamingResponse( + sessionId, + chunk, + result.getCurrentIndex() - 1, + result.isComplete(), + protocolVersion + ); + + log.debug("Streaming chunk {}/{}: {}", + result.getCurrentIndex() - 1, + result.getTotalChunks(), + chunk.substring(0, Math.min(50, chunk.length()))); + + ByteBuf chunkBuf = Unpooled.copiedBuffer(responseJson + "\n", CharsetUtil.UTF_8); + ctx.writeAndFlush(new DefaultHttpContent(chunkBuf)); + + // 如果完成,结束流式传输 + if (result.isComplete()) { + ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); + log.info("Streaming completed for session: {} with {} chunks", + sessionId, result.getTotalChunks()); + return; + } + } + } catch (Exception e) { + log.error("Error in streaming for session: {}", sessionId, e); + sendStreamingError(ctx, sessionId, e.getMessage(), protocolVersion); + } + }, 0, 300, TimeUnit.MILLISECONDS); // 稍微降低频率到300ms + } + + + /** + * 提取用户输入内容 + */ + private String extractUserContent(List> inputs) { + if (inputs == null || inputs.isEmpty()) { + return ""; + } + + // 查找用户角色的消息 + for (Map input : inputs) { + if ("user".equals(input.get("role"))) { + return (String) input.get("content"); + } + } + + // 如果没有找到用户角色,返回第一个输入的内容 + return (String) inputs.get(0).get("content"); + } + + /** + * 发送流式错误 + */ + private void sendStreamingError(ChannelHandlerContext ctx, String sessionId, + String errorMessage, String protocolVersion) { + try { + // 使用验证器创建标准错误响应 + String errorJson = McpResponseValidator.createErrorResponse( + sessionId, errorMessage, "STREAMING_ERROR", protocolVersion); + + ByteBuf errorBuf = Unpooled.copiedBuffer(errorJson + "\n", CharsetUtil.UTF_8); + ctx.writeAndFlush(new DefaultHttpContent(errorBuf)); + ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); + + log.error("Sent streaming error for session {}: {}", sessionId, errorMessage); + + } catch (Exception e) { + log.error("Error sending streaming error for session: {}", sessionId, e); + // 最后的备用方案 + String fallbackError = "{\"error\":{\"message\":\"Internal error\",\"code\":\"INTERNAL_ERROR\"},\"session_id\":\"" + sessionId + "\"}\n"; + ByteBuf fallbackBuf = Unpooled.copiedBuffer(fallbackError, CharsetUtil.UTF_8); + ctx.writeAndFlush(new DefaultHttpContent(fallbackBuf)); + ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); + } + } + + /** + * 获取或创建会话 + */ + private MCPSession getOrCreateSession(String sessionId) { + if (sessionId == null || sessionId.isEmpty()) { + // 如果没有提供会话ID,创建新会话 + MCPSession newSession = sessionManager.createSession(); + log.info("Created new session: {}", newSession.getSessionId()); + return newSession; + } + + // 尝试获取现有会话 + MCPSession session = sessionManager.getSession(sessionId); + if (session == null) { + // 如果会话不存在,使用提供的会话ID创建新会话 + session = sessionManager.createSessionWithId(sessionId); + log.info("Created session with provided ID: {}", sessionId); + } else { + log.debug("Using existing session: {}", sessionId); + } + + return session; + } + + /** + * 发送错误响应 + */ + private void sendError(ChannelHandlerContext ctx, String message) { + try { + Map errorResponse = new HashMap<>(); + errorResponse.put("error", Map.of( + "message", message, + "code", "INVALID_REQUEST" + )); + errorResponse.put("metadata", Map.of( + "timestamp", System.currentTimeMillis(), + "version", "2025-03-26" + )); + + String json = objectMapper.writeValueAsString(errorResponse); + ByteBuf content = Unpooled.copiedBuffer(json, CharsetUtil.UTF_8); + + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST, content); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); + response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + + ctx.writeAndFlush(response); + } catch (Exception e) { + log.error("Error sending error response", e); + } + } + + /** + * 发送404响应 + */ + private void sendNotFound(ChannelHandlerContext ctx) { + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); + + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + ctx.writeAndFlush(response); + } + + /** + * 处理SSE请求 + */ + private void handleSSERequest(ChannelHandlerContext ctx, FullHttpRequest request) { + log.info("Handling SSE request from: {}", ctx.channel().remoteAddress()); + + // 设置SSE响应头 + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/event-stream"); + response.headers().set(HttpHeaderNames.CACHE_CONTROL, "no-cache"); + response.headers().set(HttpHeaderNames.CONNECTION, "keep-alive"); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Cache-Control"); + + // 发送响应头 + ctx.write(response); + + // 发送初始连接事件 + sendSSEEvent(ctx, "connected", Map.of( + "status", "connected", + "server", "EventMesh MCP Server", + "version", "2025-03-26", + "timestamp", System.currentTimeMillis() + )); + + // 设置定时心跳 + ctx.executor().scheduleAtFixedRate(() -> { + if (ctx.channel().isActive()) { + sendSSEEvent(ctx, "heartbeat", Map.of( + "timestamp", System.currentTimeMillis(), + "status", "alive" + )); + } + }, 30, 30, java.util.concurrent.TimeUnit.SECONDS); + + // 发送服务器能力信息 + sendSSEEvent(ctx, "capabilities", Map.of( + "tools", Arrays.asList("text-generator", "code-analyzer", "data-processor"), + "streaming", true, + "protocol_version", "2025-03-26" + )); + } + + /** + * 发送SSE事件 + */ + private void sendSSEEvent(ChannelHandlerContext ctx, String eventType, Object data) { + try { + String eventData = objectMapper.writeValueAsString(data); + String sseEvent = String.format("event: %s\ndata: %s\n\n", eventType, eventData); + + ByteBuf eventBuf = Unpooled.copiedBuffer(sseEvent, CharsetUtil.UTF_8); + ctx.writeAndFlush(new DefaultHttpContent(eventBuf)); + + log.debug("Sent SSE event: {} with data: {}", eventType, eventData); + } catch (Exception e) { + log.error("Error sending SSE event", e); + } + } + + /** + * 发送CORS预检响应 + */ + private void sendCorsResponse(ChannelHandlerContext ctx) { + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS"); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, Authorization, Mcp-Protocol-Version, Mcp-Session-Id"); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_MAX_AGE, "86400"); + + ctx.writeAndFlush(response); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + log.error("Exception caught in channel", cause); + ctx.close(); + } + + /** + * 发送健康检查响应 + */ + private void sendHealthCheckResponse(ChannelHandlerContext ctx) { + try { + Map healthResponse = new HashMap<>(); + healthResponse.put("status", "healthy"); + healthResponse.put("service", "EventMesh MCP Server"); + healthResponse.put("version", "2025-03-26"); + healthResponse.put("timestamp", System.currentTimeMillis()); + healthResponse.put("endpoints", Map.of( + "mcp", "POST /test", + "health", "GET /test", + "sse", "GET /test (with Accept: text/event-stream)" + )); + + String json = objectMapper.writeValueAsString(healthResponse); + ByteBuf content = Unpooled.copiedBuffer(json, CharsetUtil.UTF_8); + + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); + response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, OPTIONS"); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, Mcp-Protocol-Version, Mcp-Session-Id, Accept"); + + ctx.writeAndFlush(response); + log.debug("Sent health check response"); + } catch (Exception e) { + log.error("Error sending health check response", e); + sendError(ctx, "Health check failed"); + } + } + + /** + * 发送405响应 + */ + private void sendMethodNotAllowed(ChannelHandlerContext ctx) { + try { + Map errorResponse = new HashMap<>(); + errorResponse.put("error", "Method not allowed"); + errorResponse.put("message", "This endpoint only accepts POST requests for MCP calls and GET requests for health checks"); + errorResponse.put("allowed_methods", Arrays.asList("GET", "POST", "OPTIONS")); + + String json = objectMapper.writeValueAsString(errorResponse); + ByteBuf content = Unpooled.copiedBuffer(json, CharsetUtil.UTF_8); + + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.METHOD_NOT_ALLOWED, content); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); + response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + response.headers().set(HttpHeaderNames.ALLOW, "GET, POST, OPTIONS"); + + ctx.writeAndFlush(response); + } catch (Exception e) { + log.error("Error sending method not allowed response", e); + } + } +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java index 71b9b7cb1f..61a8ebd39c 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java @@ -1,58 +1,144 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.source.protocol; +import io.netty.channel.ChannelHandler; import io.vertx.ext.web.Route; +import org.apache.eventmesh.common.config.connector.mcp.McpSourceConfig; import org.apache.eventmesh.common.config.connector.mcp.SourceConnectorConfig; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; import java.util.concurrent.BlockingQueue; - /** - * Protocol Interface. - * All protocols should implement this interface. + * Protocol interface for MCP connectors + * + * 定义了协议处理器的基本接口,支持不同的传输层实现(Netty/Vert.x) */ public interface Protocol { - /** - * Initialize the protocol. + * 初始化协议处理器 * - * @param sourceConnectorConfig source connector config + * @param sourceConnectorConfig 源连接器配置 */ void initialize(SourceConnectorConfig sourceConnectorConfig); + /** + * 初始化协议处理器(带队列) + * + * @param sourceConnectorConfig 源连接器配置 + * @param queue 请求队列 + */ + default void initialize(SourceConnectorConfig sourceConnectorConfig, BlockingQueue queue) { + initialize(sourceConnectorConfig); + } /** - * Handle the protocol message. + * 创建 Netty 请求处理器 * - * @param route route - * @param queue queue info + * @param sourceConfig MCP源配置 + * @return Netty ChannelHandler */ - void setHandler(Route route, BlockingQueue queue); + default ChannelHandler createHandler(McpSourceConfig sourceConfig) { + throw new UnsupportedOperationException("Netty handler creation not supported by this protocol implementation"); + } + /** + * 设置 Vert.x 路由处理器 + * + * @deprecated 推荐使用 createHandler(McpSourceConfig) 来创建 Netty 处理器 + * @param route Vert.x 路由 + * @param queue 请求队列 + */ + @Deprecated + default void setHandler(Route route, BlockingQueue queue) { + throw new UnsupportedOperationException("Vert.x route handling not supported by this protocol implementation"); + } /** - * Convert the message to ConnectRecord. + * 将消息转换为 ConnectRecord * - * @param message message - * @return ConnectRecord + * @param message 原始消息 + * @return 转换后的 ConnectRecord */ ConnectRecord convertToConnectRecord(Object message); -} + + /** + * 提交记录处理结果 + * + * @param record 要提交的记录 + */ + default void commit(ConnectRecord record) { + // 默认实现为空,子类可以重写 + } + + /** + * 处理记录异常 + * + * @param record 发生异常的记录 + */ + default void onException(ConnectRecord record) { + // 默认实现为空,子类可以重写 + } + + /** + * 关闭协议处理器 + */ + default void shutdown() { + // 默认实现为空,子类可以重写 + } + + /** + * 获取协议名称 + * + * @return 协议名称 + */ + default String getProtocolName() { + return this.getClass().getSimpleName(); + } + + /** + * 获取协议版本 + * + * @return 协议版本 + */ + default String getProtocolVersion() { + return "1.0"; + } + + /** + * 检查协议是否支持流式传输 + * + * @return 是否支持流式传输 + */ + default boolean supportsStreaming() { + return false; + } + + /** + * 检查协议是否支持压缩 + * + * @return 是否支持压缩 + */ + default boolean supportsCompression() { + return false; + } + + /** + * 获取协议配置信息 + * + * @return 配置信息映射 + */ + default java.util.Map getProtocolConfig() { + return java.util.Collections.emptyMap(); + } + + /** + * 验证协议配置 + * + * @param config 要验证的配置 + * @return 验证结果 + */ + default boolean validateConfig(SourceConnectorConfig config) { + return true; + } +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java index 140395f843..2c342cd8b9 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java @@ -1,121 +1,460 @@ package org.apache.eventmesh.connector.mcp.source.protocol.impl; -import com.fasterxml.jackson.core.type.TypeReference; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.json.JsonObject; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.channel.ChannelHandler; import io.vertx.ext.web.Route; -import io.vertx.ext.web.handler.BodyHandler; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.apache.eventmesh.common.Constants; +import org.apache.eventmesh.common.config.connector.mcp.McpSourceConfig; import org.apache.eventmesh.common.config.connector.mcp.SourceConnectorConfig; -import org.apache.eventmesh.common.utils.JsonUtils; import org.apache.eventmesh.connector.mcp.source.data.McpRequest; -import org.apache.eventmesh.connector.mcp.source.data.McpResponse; +import org.apache.eventmesh.connector.mcp.source.handler.NettyMcpRequestHandler; import org.apache.eventmesh.connector.mcp.source.protocol.Protocol; +import org.apache.eventmesh.connector.mcp.util.AIToolManager; +import org.apache.eventmesh.connector.mcp.session.MCPSession; +import org.apache.eventmesh.connector.mcp.session.SessionManager; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; -import java.util.Base64; -import java.util.Map; +import org.apache.eventmesh.connector.mcp.util.CloudEventsExtensionUtil; + +import java.util.*; import java.util.concurrent.BlockingQueue; -import java.util.stream.Collectors; -// 解析标准 2025-03-26 spec 请求 /** - * Mcp Protocol. This class represents the mcp protocol. The processing method of this class does not perform any other operations - * except storing the request and returning a general response. + * MCP Standard Protocol Implementation (Netty-based) + * + * 职责: + * 1. MCP协议的具体实现和规范遵循 + * 2. 请求/响应格式的解析和构造 + * 3. 数据转换和验证 + * 4. 协议级别的错误处理 */ @Slf4j public class McpStandardProtocol implements Protocol { + public static final String PROTOCOL_NAME = "Mcp"; + public static final String PROTOCOL_VERSION = "2025-03-26"; private SourceConnectorConfig sourceConnectorConfig; + private BlockingQueue queue; + // Getter methods + @Getter + private SessionManager sessionManager; + @Getter + private AIToolManager toolManager; + @Getter + private ObjectMapper objectMapper; /** - * Initialize the protocol - * - * @param sourceConnectorConfig source connector config + * 初始化协议处理器 */ @Override public void initialize(SourceConnectorConfig sourceConnectorConfig) { this.sourceConnectorConfig = sourceConnectorConfig; + this.sessionManager = new SessionManager(); + this.toolManager = new AIToolManager(); + this.objectMapper = new ObjectMapper(); + + log.info("McpStandardProtocol initialized - version: {}", PROTOCOL_VERSION); + } + + /** + * 初始化协议处理器(带队列) + */ + @Override + public void initialize(SourceConnectorConfig sourceConnectorConfig, BlockingQueue queue) { + this.queue = queue; + initialize(sourceConnectorConfig); } /** - * Set the handler for the route - * - * @param route route - * @param queue queue info + * 创建 Netty 请求处理器 */ @Override + public ChannelHandler createHandler(McpSourceConfig sourceConfig) { + if (sessionManager == null || toolManager == null || objectMapper == null) { + throw new IllegalStateException("Protocol not properly initialized"); + } + return new NettyMcpRequestHandler(sourceConfig, queue, sessionManager, objectMapper, toolManager); + } + + /** + * 设置 Vert.x 路由处理器(已弃用,保留兼容性) + */ + @Override + @Deprecated public void setHandler(Route route, BlockingQueue queue) { - route.method(HttpMethod.POST) - .handler(BodyHandler.create()) - .handler(ctx -> { - // Get the payload - String jsonStr = ctx.body().asString(Constants.DEFAULT_CHARSET.toString()); - Map payloadMap = JsonUtils.parseObject(jsonStr, new TypeReference>() {}); - - // Create and store the mcp request - Map headerMap = ctx.request().headers().entries().stream() - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - McpRequest mcpRequest = new McpRequest(PROTOCOL_NAME, ctx.request().absoluteURI(), headerMap, true, payloadMap, ctx); - if (!queue.offer(mcpRequest)) { - throw new IllegalStateException("Failed to store the request."); - } - - if (sourceConnectorConfig.isDataConsistencyEnabled()) { - // Return 200 OK - ctx.response() - .setStatusCode(HttpResponseStatus.OK.code()) - .end(McpResponse.success(payloadMap.get("inputs"), (String) payloadMap.get("session_id")).toJsonStr()); - //.end(McpResponse.success(null, "0").toJsonStr()); - } - - }) - .failureHandler(ctx -> { - log.error("Failed to handle the request. ", ctx.failure()); - - // Return Bad Response - ctx.response() - .setStatusCode(ctx.statusCode()) - .end(McpResponse.base(null, "0", ctx.failure().getMessage()).toJsonStr()); // todo - }); - - } - - /** - * Convert the message to a connect record - * - * @param message message - * @return connect record + log.warn("setHandler(Route, BlockingQueue) is deprecated in Netty implementation. Use createHandler(McpSourceConfig) instead."); + throw new UnsupportedOperationException("Vert.x Route handling is not supported in Netty implementation"); + } + + /** + * 转换消息为 ConnectRecord */ @Override public ConnectRecord convertToConnectRecord(Object message) { + if (!(message instanceof McpRequest)) { + log.warn("Received non-McpRequest message: {}", message != null ? message.getClass().getSimpleName() : "null"); + return null; + } + McpRequest request = (McpRequest) message; - ConnectRecord connectRecord = new ConnectRecord(null, null, System.currentTimeMillis(), request.getInputs()); - connectRecord.addExtension("source", request.getProtocolName()); - request.getMetadata().forEach((k, v) -> { - if (k.equalsIgnoreCase("extension")) { - JsonObject extension = new JsonObject(v); - extension.forEach(e -> connectRecord.addExtension(e.getKey(), e.getValue())); + + try { + // 确保会话存在 + ensureSessionExists(request); + + // 验证请求基本格式 + validateRequestFormat(request); + + // 提取数据 + Object data = extractDataFromRequest(request); + + // 创建 ConnectRecord + ConnectRecord connectRecord = new ConnectRecord( + null, // partition + null, // offset + System.currentTimeMillis(), // timestamp + data // data + ); + + // 添加协议相关扩展信息 + enrichConnectRecord(connectRecord, request); + + log.debug("Converted McpRequest to ConnectRecord: sessionId={}, recordId={}", + request.getSessionId(), connectRecord.getRecordId()); + + return connectRecord; + + } catch (Exception e) { + log.error("Failed to convert McpRequest to ConnectRecord for session: {}", + request.getSessionId(), e); + return null; + } + } + + /** + * 确保会话存在 + */ + private void ensureSessionExists(McpRequest request) { + String sessionId = request.getSessionId(); + MCPSession session = request.getSession(); + + // 如果请求中已经有会话对象,直接使用 + if (session != null) { + sessionManager.putSession(session); + return; + } + + // 如果有会话ID,尝试获取或创建会话 + if (sessionId != null && !sessionId.trim().isEmpty()) { + session = sessionManager.getSession(sessionId); + if (session == null) { + session = sessionManager.createSessionWithId(sessionId); + log.info("Created new session with provided ID: {}", sessionId); + } + request.setSession(session); + } else { + // 如果没有会话ID,创建新会话 + session = sessionManager.createSession(); + request.setSessionId(session.getSessionId()); + request.setSession(session); + log.info("Created new session: {}", session.getSessionId()); + } + } + + /** + * 验证请求格式 + */ + private void validateRequestFormat(McpRequest request) { + if (request.getPayloadMap() == null) { + throw new IllegalArgumentException("Request payload cannot be null"); + } + + // 验证协议版本(可选) + String protocolVersion = request.getProtocolVersion(); + if (protocolVersion != null && !PROTOCOL_VERSION.equals(protocolVersion)) { + log.warn("Protocol version mismatch: expected {}, got {}", PROTOCOL_VERSION, protocolVersion); + } + + // 验证基本字段 + Map payload = request.getPayloadMap(); + if (!payload.containsKey("inputs") && !payload.containsKey("tools")) { + log.warn("Request payload missing expected fields (inputs or tools)"); + } + } + + /** + * 从请求中提取数据 + */ + private Object extractDataFromRequest(McpRequest request) { + Map payload = request.getPayloadMap(); + + // 优先使用 inputs 作为主要数据 + Object inputs = payload.get("inputs"); + if (inputs != null) { + return inputs; + } + + // 如果没有 inputs,检查是否有 tools 字段 + Object tools = payload.get("tools"); + if (tools != null) { + return tools; + } + + // 如果都没有,使用整个 payload + return payload; + } + + /** + * 丰富 ConnectRecord 的扩展信息(使用CloudEvents兼容的方式) + */ + private void enrichConnectRecord(ConnectRecord connectRecord, McpRequest request) { + // 添加MCP标准扩展属性 + CloudEventsExtensionUtil.addMcpStandardExtensions( + connectRecord, + request.getSessionId(), + PROTOCOL_VERSION, + request.isStreaming() + ); + + // 添加基本信息 + CloudEventsExtensionUtil.addExtensionSafely(connectRecord, "source", "mcpconnector"); + CloudEventsExtensionUtil.addExtensionSafely(connectRecord, "requesturi", request.getAbsoluteURI()); + + // 添加会话信息 + MCPSession session = request.getSession(); + if (session != null) { + CloudEventsExtensionUtil.addExtensionSafely(connectRecord, "sessioncreated", String.valueOf(session.getLastActivity())); + Object context = session.getContext("context"); + if (context != null) { + CloudEventsExtensionUtil.addExtensionSafely(connectRecord, "sessioncontext", context.toString()); + } + } + + // 添加请求头信息 + Map headers = request.getHeaderMap(); + if (headers != null) { + CloudEventsExtensionUtil.addHeaderExtensions(connectRecord, headers); + } + + // 添加Payload元数据 + Map payload = request.getPayloadMap(); + if (payload != null) { + // 添加context信息 + Object context = payload.get("context"); + if (context != null) { + CloudEventsExtensionUtil.addExtensionSafely(connectRecord, "payloadcontext", context.toString()); + } + + // 添加metadata信息 + Object metadataObj = payload.get("metadata"); + if (metadataObj instanceof Map) { + Map metadata = (Map) metadataObj; + CloudEventsExtensionUtil.addMetadataExtensions(connectRecord, metadata); + } + + // 添加流式标识 + Object stream = payload.get("stream"); + if (stream != null) { + CloudEventsExtensionUtil.addExtensionSafely(connectRecord, "streamrequest", stream.toString()); + } + } + + // 生成唯一记录ID + String recordUniqueId = generateRecordUniqueId(request); + CloudEventsExtensionUtil.addExtensionSafely(connectRecord, "recordid", recordUniqueId); + + // 添加处理提示 + String processingHint = getProcessingHint(request); + CloudEventsExtensionUtil.addExtensionSafely(connectRecord, "processhint", processingHint); + + log.debug("Enriched ConnectRecord with CloudEvents-compatible extensions for session: {}", request.getSessionId()); + } + + /** + * 生成唯一记录ID + */ + private String generateRecordUniqueId(McpRequest request) { + return String.format("%s_%s_%d", + PROTOCOL_NAME, + request.getSessionId(), + System.currentTimeMillis()); + } + + /** + * 获取处理提示 + */ + private String getProcessingHint(McpRequest request) { + if (request.isStreaming()) { + return "streaming"; + } + + Map payload = request.getPayloadMap(); + if (payload != null && payload.containsKey("metadata")) { + Object metadataObj = payload.get("metadata"); + if (metadataObj instanceof Map) { + Map metadata = (Map) metadataObj; + Object priority = metadata.get("priority"); + if (priority != null) { + return "priority:" + priority; + } } - }); - // check recordUniqueId - if (!connectRecord.getExtensions().containsKey("recordUniqueId")) { - connectRecord.addExtension("recordUniqueId", connectRecord.getRecordId()); } - // check data - if (connectRecord.getExtensionObj("isBase64") != null) { - if (Boolean.parseBoolean(connectRecord.getExtensionObj("isBase64").toString())) { - byte[] data = Base64.getDecoder().decode(connectRecord.getData().toString()); - connectRecord.setData(data); + return "normal"; + } + + /** + * 提交记录 + */ + @Override + public void commit(ConnectRecord record) { + try { + String sessionId = (String) record.getExtensionObj("sessionId"); + if (sessionId != null && sessionManager != null) { + MCPSession session = sessionManager.getSession(sessionId); + if (session != null) { + session.updateLastActivity(); + } } + + log.debug("Committed record: {}", record.getRecordId()); + + } catch (Exception e) { + log.error("Failed to commit record: {}", record.getRecordId(), e); + } + } + + /** + * 处理异常 + */ + @Override + public void onException(ConnectRecord record) { + try { + String sessionId = (String) record.getExtensionObj("sessionId"); + log.error("Exception occurred for record: {}, sessionId: {}", record.getRecordId(), sessionId); + + // 记录错误统计或执行恢复逻辑 + + } catch (Exception e) { + log.error("Failed to handle exception for record: {}", record.getRecordId(), e); } - if (request.getRoutingContext() != null) { - connectRecord.addExtension("routingContext", request.getRoutingContext()); + } + + /** + * 关闭协议处理器 + */ + @Override + public void shutdown() { + try { + log.info("Shutting down McpStandardProtocol..."); + + // 关闭会话管理器 + if (sessionManager != null) { + sessionManager.shutdown(); + } + + log.info("McpStandardProtocol shutdown completed"); + + } catch (Exception e) { + log.error("Error during protocol shutdown", e); } - return connectRecord; } -} + + /** + * 获取协议统计信息 + */ + public Map getProtocolStats() { + Map stats = new HashMap<>(); + stats.put("protocolName", PROTOCOL_NAME); + stats.put("protocolVersion", PROTOCOL_VERSION); + + // 添加会话统计 + if (sessionManager != null) { + // 可以添加会话数量等统计信息 + stats.put("sessionManagerInitialized", true); + } + + return stats; + } + + /** + * 创建成功响应 + */ + public String createSuccessResponse(Object data, String sessionId) { + try { + Map response = new HashMap<>(); + response.put("session_id", sessionId); + response.put("outputs", data); + + Map metadata = new HashMap<>(); + metadata.put("timestamp", System.currentTimeMillis()); + metadata.put("version", PROTOCOL_VERSION); + metadata.put("status", "success"); + response.put("metadata", metadata); + + return objectMapper.writeValueAsString(response); + + } catch (Exception e) { + log.error("Failed to create success response", e); + return createErrorResponse("Failed to create response", sessionId); + } + } + + /** + * 创建错误响应 + */ + public String createErrorResponse(String errorMessage, String sessionId) { + try { + Map response = new HashMap<>(); + response.put("session_id", sessionId); + response.put("error", Map.of( + "message", errorMessage, + "code", "PROCESSING_ERROR" + )); + + Map metadata = new HashMap<>(); + metadata.put("timestamp", System.currentTimeMillis()); + metadata.put("version", PROTOCOL_VERSION); + metadata.put("status", "error"); + response.put("metadata", metadata); + + return objectMapper.writeValueAsString(response); + + } catch (Exception e) { + log.error("Failed to create error response", e); + return "{\"error\":\"Internal server error\"}"; + } + } + + @Override + public String getProtocolName() { + return PROTOCOL_NAME; + } + + @Override + public String getProtocolVersion() { + return PROTOCOL_VERSION; + } + + @Override + public boolean supportsStreaming() { + return true; + } + + @Override + public boolean supportsCompression() { + return false; + } + + @Override + public Map getProtocolConfig() { + return getProtocolStats(); + } + + @Override + public boolean validateConfig(SourceConnectorConfig config) { + return config != null; + } +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/AIToolManager.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/AIToolManager.java new file mode 100644 index 0000000000..a27e50270e --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/AIToolManager.java @@ -0,0 +1,162 @@ +package org.apache.eventmesh.connector.mcp.util; + +import org.apache.eventmesh.connector.mcp.session.MCPSession; +import lombok.extern.slf4j.Slf4j; +import org.apache.eventmesh.connector.mcp.source.data.MCPStreamingResponse; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * AI工具管理器 + * 负责处理AI相关的请求和流式响应 + */ +@Slf4j +public class AIToolManager { + + /** + * 处理普通请求 + * + * @param userContent 用户输入内容 + * @param session MCP会话 + * @param metadata 元数据 + * @return 处理结果的Future + */ + public CompletableFuture processRequest(String userContent, MCPSession session, Map metadata) { + return CompletableFuture.supplyAsync(() -> { + try { + log.info("Processing request for session: {}, content: {}", session.getSessionId(), userContent); + + // 模拟AI处理 + String response = "Based on your input: '" + userContent + "', here's my response. "; + + // 考虑会话上下文 + List history = (List) session.getContext("history"); + if (history != null && !history.isEmpty()) { + response += "I can see from our conversation history that we've discussed: " + String.join(", ", history) + ". "; + } + + response += "This is a comprehensive response to your query."; + + // 模拟处理时间 + Thread.sleep(500); + + log.info("Request processed successfully for session: {}", session.getSessionId()); + return response; + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Request processing interrupted for session: {}", session.getSessionId(), e); + return "Processing was interrupted."; + } catch (Exception e) { + log.error("Error processing request for session: {}", session.getSessionId(), e); + return "Error occurred while processing your request."; + } + }); + } + + /** + * 处理流式请求 + * + * @param userContent 用户输入内容 + * @param session MCP会话 + * @param metadata 元数据 + * @return 流式结果的Future + */ + public CompletableFuture processStreamingRequest(String userContent, MCPSession session, Map metadata) { + return CompletableFuture.supplyAsync(() -> { + try { + log.info("Processing streaming request for session: {}, content: {}", session.getSessionId(), userContent); + + // 模拟AI流式处理 + String response = "正在分析您的输入: " + userContent + " 生成回复中... 这是一个很好的问题。 让我从几个角度来分析: 首先,技术层面的考虑... 其次,实际应用场景... 然后,未来发展趋势... 最后,我的具体建议... 综合来看,我认为... 希望这个回答对您有帮助!"; + + // 考虑会话上下文 + List history = (List) session.getContext("history"); + if (history != null && !history.isEmpty()) { + response += " 结合我们之前的对话历史,我认为... 这与之前讨论的" + String.join("、", history) + "相呼应。"; + } + + // 从metadata中获取额外信息 + if (metadata != null) { + String userId = (String) metadata.get("user_id"); + if (userId != null) { + response += " 针对用户" + userId + "的个性化建议..."; + } + } + + log.info("Streaming request processed successfully for session: {}", session.getSessionId()); + return new MCPStreamingResponse(response); + + } catch (Exception e) { + log.error("Error processing streaming request for session: {}", session.getSessionId(), e); + return new MCPStreamingResponse("Error occurred while processing your streaming request."); + } + }); + } + + /** + * 处理工具调用请求 + * + * @param toolName 工具名称 + * @param input 输入参数 + * @param session MCP会话 + * @return 处理结果的Future + */ + public CompletableFuture callTool(String toolName, String input, MCPSession session) { + return CompletableFuture.supplyAsync(() -> { + try { + log.info("Calling tool: {} for session: {}", toolName, session.getSessionId()); + + switch (toolName) { + case "text-generator": + return generateText(input, session); + case "code-analyzer": + return analyzeCode(input, session); + case "data-processor": + return processData(input, session); + default: + log.warn("Unknown tool: {}", toolName); + return "Unknown tool: " + toolName; + } + + } catch (Exception e) { + log.error("Error calling tool: {} for session: {}", toolName, session.getSessionId(), e); + return "Error occurred while calling tool: " + toolName; + } + }); + } + + /** + * 文本生成工具 + */ + private String generateText(String input, MCPSession session) { + // 模拟文本生成 + return "Generated text based on input: " + input + ". Session: " + session.getSessionId(); + } + + /** + * 代码分析工具 + */ + private String analyzeCode(String input, MCPSession session) { + // 模拟代码分析 + return "Code analysis result for: " + input + + "\n- Syntax: OK" + + "\n- Performance: Good" + + "\n- Security: No issues found" + + "\n- Session: " + session.getSessionId(); + } + + /** + * 数据处理工具 + */ + private String processData(String input, MCPSession session) { + // 模拟数据处理 + return "Data processing result for: " + input + + "\n- Records processed: 1000" + + "\n- Success rate: 98.5%" + + "\n- Processing time: 2.3s" + + "\n- Session: " + session.getSessionId(); + } +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/CloudEventsExtensionUtil.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/CloudEventsExtensionUtil.java new file mode 100644 index 0000000000..3602847180 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/CloudEventsExtensionUtil.java @@ -0,0 +1,213 @@ +package org.apache.eventmesh.connector.mcp.util; + +import lombok.extern.slf4j.Slf4j; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; + +import java.util.Map; +import java.util.regex.Pattern; + +/** + * CloudEvents扩展属性工具类 + * + * 根据CloudEvents规范处理扩展属性: + * - 扩展名称必须是小写字母、数字和短横线的组合 + * - 必须以字母开头 + * - 长度在1-20个字符之间 + * - 不能包含点号、下划线等特殊字符 + */ +@Slf4j +public class CloudEventsExtensionUtil { + + // CloudEvents扩展名称的正则表达式 - 只允许小写字母和数字 + private static final Pattern VALID_EXTENSION_NAME = Pattern.compile("^[a-z][a-z0-9]{0,19}$"); + + /** + * 安全地添加扩展属性到ConnectRecord + * + * @param record ConnectRecord对象 + * @param name 扩展属性名称 + * @param value 扩展属性值 + */ + public static void addExtensionSafely(ConnectRecord record, String name, Object value) { + if (record == null || name == null) { + return; + } + + String safeName = normalizeExtensionName(name); + String safeValue = normalizeExtensionValue(value); + + try { + record.addExtension(safeName, safeValue); + log.debug("Added extension: {} = {}", safeName, safeValue); + } catch (Exception e) { + log.warn("Failed to add extension {}: {}", safeName, e.getMessage()); + } + } + + /** + * 批量添加扩展属性 + * + * @param record ConnectRecord对象 + * @param extensions 扩展属性映射 + * @param prefix 前缀 + */ + public static void addExtensionsBatch(ConnectRecord record, Map extensions, String prefix) { + if (record == null || extensions == null) { + return; + } + + String safePrefix = prefix != null ? normalizeExtensionName(prefix) : ""; + + extensions.forEach((key, value) -> { + String fullName = safePrefix + normalizeExtensionName(key); + addExtensionSafely(record, fullName, value); + }); + } + + /** + * 规范化扩展名称以符合CloudEvents规范 + * + * CloudEvents扩展名称规则(更严格): + * - 只能包含小写字母(a-z)和数字(0-9) + * - 必须以小写字母开头 + * - 长度在1-20个字符之间 + * - 不能包含短横线、下划线、点号等特殊字符 + * + * @param name 原始名称 + * @return 规范化后的名称 + */ + public static String normalizeExtensionName(String name) { + if (name == null || name.isEmpty()) { + return "unknown"; + } + + // 转换为小写 + String normalized = name.toLowerCase(); + + // 移除所有非字母数字字符 + normalized = normalized.replaceAll("[^a-z0-9]", ""); + + // 确保以字母开头 + if (normalized.isEmpty() || !Character.isLetter(normalized.charAt(0))) { + normalized = "ext" + normalized; + } + + // 限制长度为20个字符 + if (normalized.length() > 20) { + normalized = normalized.substring(0, 20); + } + + // 最终检查,确保不为空且有效 + if (normalized.isEmpty() || !isValidExtensionName(normalized)) { + normalized = "ext"; + } + + return normalized; + } + + /** + * 规范化扩展值 + * + * @param value 原始值 + * @return 规范化后的字符串值 + */ + public static String normalizeExtensionValue(Object value) { + if (value == null) { + return ""; + } + + String stringValue = value.toString(); + + // CloudEvents扩展值必须是字符串,且不能包含控制字符 + return stringValue.replaceAll("[\\p{Cntrl}]", " ").trim(); + } + + /** + * 验证扩展名称是否符合CloudEvents规范 + * + * @param name 扩展名称 + * @return 是否有效 + */ + public static boolean isValidExtensionName(String name) { + return name != null && VALID_EXTENSION_NAME.matcher(name).matches(); + } + + /** + * 创建MCP特定的扩展属性前缀 + * + * @param category 类别 + * @return 规范化的前缀 + */ + public static String createMcpPrefix(String category) { + return normalizeExtensionName("mcp" + category); + } + + /** + * 为MCP协议添加标准扩展属性 + * + * @param record ConnectRecord对象 + * @param sessionId 会话ID + * @param protocolVersion 协议版本 + * @param isStreaming 是否流式 + */ + public static void addMcpStandardExtensions(ConnectRecord record, String sessionId, + String protocolVersion, boolean isStreaming) { + addExtensionSafely(record, "mcpprotocol", "mcp"); + addExtensionSafely(record, "mcpversion", protocolVersion); + addExtensionSafely(record, "mcpsession", sessionId); + addExtensionSafely(record, "mcpstreaming", String.valueOf(isStreaming)); + addExtensionSafely(record, "mcptimestamp", String.valueOf(System.currentTimeMillis())); + } + + /** + * 为请求头添加扩展属性 + * + * @param record ConnectRecord对象 + * @param headers 请求头映射 + */ + public static void addHeaderExtensions(ConnectRecord record, Map headers) { + if (headers == null || headers.isEmpty()) { + return; + } + + headers.forEach((key, value) -> { + String extensionName = "header" + normalizeExtensionName(key); + addExtensionSafely(record, extensionName, value); + }); + } + + /** + * 为元数据添加扩展属性 + * + * @param record ConnectRecord对象 + * @param metadata 元数据映射 + */ + public static void addMetadataExtensions(ConnectRecord record, Map metadata) { + if (metadata == null || metadata.isEmpty()) { + return; + } + + metadata.forEach((key, value) -> { + String extensionName = "meta" + normalizeExtensionName(key); + addExtensionSafely(record, extensionName, value); + }); + } + + /** + * 获取所有有效的扩展名称示例(用于测试和文档) + * + * @return 有效扩展名称示例列表 + */ + public static String[] getValidExtensionExamples() { + return new String[]{ + "mcpprotocol", + "mcpversion", + "mcpsession", + "mcpstreaming", + "headercontenttype", + "metauserid", + "requesttimestamp", + "processinghint" + }; + } +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/McpResponseValidator.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/McpResponseValidator.java new file mode 100644 index 0000000000..24ec512478 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/McpResponseValidator.java @@ -0,0 +1,311 @@ +package org.apache.eventmesh.connector.mcp.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * MCP响应格式验证器 + * 确保生成的响应完全符合MCP 2025-03-26规范 + */ +@Slf4j +public class McpResponseValidator { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final String PROTOCOL_VERSION = "2025-03-26"; + + /** + * 创建标准的MCP流式响应 + * + * @param sessionId 会话ID + * @param content 内容 + * @param chunkIndex 块索引 + * @param isComplete 是否完成 + * @param protocolVersion 协议版本 + * @return 格式化的响应JSON字符串 + */ + public static String createStreamingResponse(String sessionId, String content, + int chunkIndex, boolean isComplete, + String protocolVersion) { + try { + Map response = new LinkedHashMap<>(); + + // outputs 数组 - 必须字段 + Map output = new HashMap<>(); + output.put("role", "assistant"); + output.put("content", sanitizeContent(content)); + response.put("outputs", Arrays.asList(output)); + + // metadata 对象 - 必须字段 + Map metadata = new LinkedHashMap<>(); + metadata.put("chunk_index", chunkIndex); + metadata.put("streaming", true); + metadata.put("is_complete", isComplete); + metadata.put("version", protocolVersion != null ? protocolVersion : PROTOCOL_VERSION); + metadata.put("timestamp", System.currentTimeMillis()); + response.put("metadata", metadata); + + // session_id - 必须字段 + response.put("session_id", sessionId); + + String jsonString = objectMapper.writeValueAsString(response); + + // 验证生成的JSON + if (!isValidMcpResponse(jsonString)) { + log.error("Generated invalid MCP response"); + return createFallbackResponse(sessionId, chunkIndex, isComplete); + } + + return jsonString; + + } catch (Exception e) { + log.error("Error creating streaming response", e); + return createFallbackResponse(sessionId, chunkIndex, isComplete); + } + } + + /** + * 创建标准的MCP错误响应 + * + * @param sessionId 会话ID + * @param errorMessage 错误消息 + * @param errorCode 错误代码 + * @param protocolVersion 协议版本 + * @return 格式化的错误响应JSON字符串 + */ + public static String createErrorResponse(String sessionId, String errorMessage, + String errorCode, String protocolVersion) { + try { + Map response = new LinkedHashMap<>(); + + // error 对象 - 必须字段 + Map error = new HashMap<>(); + error.put("message", sanitizeContent(errorMessage)); + error.put("code", errorCode != null ? errorCode : "UNKNOWN_ERROR"); + response.put("error", error); + + // metadata 对象 + Map metadata = new LinkedHashMap<>(); + metadata.put("streaming", false); + metadata.put("version", protocolVersion != null ? protocolVersion : PROTOCOL_VERSION); + metadata.put("timestamp", System.currentTimeMillis()); + response.put("metadata", metadata); + + // session_id + response.put("session_id", sessionId); + + return objectMapper.writeValueAsString(response); + + } catch (Exception e) { + log.error("Error creating error response", e); + return "{\"error\":{\"message\":\"Internal server error\",\"code\":\"INTERNAL_ERROR\"},\"session_id\":\"" + sessionId + "\"}"; + } + } + + /** + * 创建标准的MCP成功响应 + * + * @param sessionId 会话ID + * @param content 响应内容 + * @param protocolVersion 协议版本 + * @return 格式化的成功响应JSON字符串 + */ + public static String createSuccessResponse(String sessionId, String content, String protocolVersion) { + try { + Map response = new LinkedHashMap<>(); + + // outputs 数组 + Map output = new HashMap<>(); + output.put("role", "assistant"); + output.put("content", sanitizeContent(content)); + response.put("outputs", Arrays.asList(output)); + + // metadata 对象 + Map metadata = new LinkedHashMap<>(); + metadata.put("streaming", false); + metadata.put("version", protocolVersion != null ? protocolVersion : PROTOCOL_VERSION); + metadata.put("timestamp", System.currentTimeMillis()); + response.put("metadata", metadata); + + // session_id + response.put("session_id", sessionId); + + return objectMapper.writeValueAsString(response); + + } catch (Exception e) { + log.error("Error creating success response", e); + return createErrorResponse(sessionId, "Failed to create response", "RESPONSE_ERROR", protocolVersion); + } + } + + /** + * 验证MCP响应格式是否正确 + * + * @param jsonString JSON字符串 + * @return 是否有效 + */ + public static boolean isValidMcpResponse(String jsonString) { + try { + JsonNode root = objectMapper.readTree(jsonString); + + // 检查必须字段 + if (!root.has("session_id")) { + log.warn("Missing session_id field"); + return false; + } + + // 检查是否有error或outputs + boolean hasError = root.has("error"); + boolean hasOutputs = root.has("outputs"); + + if (!hasError && !hasOutputs) { + log.warn("Missing both error and outputs fields"); + return false; + } + + // 检查metadata + if (!root.has("metadata")) { + log.warn("Missing metadata field"); + return false; + } + + JsonNode metadata = root.get("metadata"); + if (!metadata.has("version") || !metadata.has("timestamp")) { + log.warn("Missing required metadata fields"); + return false; + } + + // 如果是流式响应,检查流式字段 + if (metadata.has("streaming") && metadata.get("streaming").asBoolean()) { + if (!metadata.has("chunk_index") || !metadata.has("is_complete")) { + log.warn("Missing required streaming metadata fields"); + return false; + } + } + + return true; + + } catch (Exception e) { + log.warn("JSON parsing error: {}", e.getMessage()); + return false; + } + } + + /** + * 清理内容,确保JSON安全 + * + * @param content 原始内容 + * @return 清理后的内容 + */ + private static String sanitizeContent(String content) { + if (content == null) { + return ""; + } + + // 移除可能导致JSON问题的字符 + return content.trim() + .replaceAll("[\r\n\t]", " ") // 替换换行和制表符 + .replaceAll("\\s+", " ") // 合并多个空格 + .replaceAll("\"", "\\\""); // 转义双引号 + } + + /** + * 创建备用响应 + * + * @param sessionId 会话ID + * @param chunkIndex 块索引 + * @param isComplete 是否完成 + * @return 备用响应JSON字符串 + */ + private static String createFallbackResponse(String sessionId, int chunkIndex, boolean isComplete) { + return String.format( + "{\"outputs\":[{\"role\":\"assistant\",\"content\":\"Error processing content\"}]," + + "\"metadata\":{\"chunk_index\":%d,\"streaming\":true,\"is_complete\":%s,\"version\":\"%s\",\"timestamp\":%d}," + + "\"session_id\":\"%s\"}", + chunkIndex, isComplete, PROTOCOL_VERSION, System.currentTimeMillis(), sessionId + ); + } + + /** + * 格式化JSON字符串(用于调试) + * + * @param jsonString JSON字符串 + * @return 格式化后的JSON字符串 + */ + public static String formatJson(String jsonString) { + try { + JsonNode jsonNode = objectMapper.readTree(jsonString); + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode); + } catch (Exception e) { + log.warn("Failed to format JSON: {}", e.getMessage()); + return jsonString; + } + } + + /** + * 验证并修复JSON字符串 + * + * @param jsonString 原始JSON字符串 + * @param sessionId 会话ID(用于修复) + * @return 修复后的JSON字符串 + */ + public static String validateAndFix(String jsonString, String sessionId) { + if (isValidMcpResponse(jsonString)) { + return jsonString; + } + + log.warn("Invalid MCP response detected, attempting to fix"); + + try { + JsonNode root = objectMapper.readTree(jsonString); + Map fixed = new LinkedHashMap<>(); + + // 确保有session_id + if (root.has("session_id")) { + fixed.put("session_id", root.get("session_id").asText()); + } else { + fixed.put("session_id", sessionId); + } + + // 处理outputs或error + if (root.has("outputs")) { + fixed.put("outputs", objectMapper.convertValue(root.get("outputs"), Object.class)); + } else if (root.has("error")) { + fixed.put("error", objectMapper.convertValue(root.get("error"), Object.class)); + } else { + Map error = new HashMap<>(); + error.put("message", "Unknown error"); + error.put("code", "UNKNOWN_ERROR"); + fixed.put("error", error); + } + + // 确保有完整的metadata + Map metadata = new LinkedHashMap<>(); + if (root.has("metadata")) { + JsonNode metaNode = root.get("metadata"); + metadata.putAll(objectMapper.convertValue(metaNode, Map.class)); + } + + // 补充缺失的metadata字段 + if (!metadata.containsKey("version")) { + metadata.put("version", PROTOCOL_VERSION); + } + if (!metadata.containsKey("timestamp")) { + metadata.put("timestamp", System.currentTimeMillis()); + } + + fixed.put("metadata", metadata); + + return objectMapper.writeValueAsString(fixed); + + } catch (Exception e) { + log.error("Failed to fix JSON: {}", e.getMessage()); + return createErrorResponse(sessionId, "JSON format error", "JSON_ERROR", PROTOCOL_VERSION); + } + } +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/McpUtils.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/McpUtils.java deleted file mode 100644 index 11db79c7a8..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/McpUtils.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.apache.eventmesh.connector.mcp.util; - -public class McpUtils { -} From 3692b6ec4b4274aa957234a367b4b092ea7f1e16 Mon Sep 17 00:00:00 2001 From: jerryyummy <461697582@qq.com> Date: Mon, 15 Sep 2025 02:03:50 -0700 Subject: [PATCH 08/36] build the basic mcp server --- .../config/connector/mcp/HttpMcpConfig.java | 48 -- .../config/connector/mcp/McpRetryConfig.java | 33 + .../config/connector/mcp/McpSinkConfig.java | 21 +- .../connector/mcp/SinkConnectorConfig.java | 77 +++ .../common/protocol/mcp/McpEventWrapper.java | 95 --- .../src/main/resources/server-config.yml | 2 +- .../eventmesh-connector-mcp/build.gradle | 8 +- .../mcp/server/McpConnectServer.java | 6 + .../connector/mcp/session/MCPSession.java | 34 - .../connector/mcp/session/SessionManager.java | 72 -- .../connector/mcp/sink/McpSinkConnector.java | 170 +++++ .../mcp/sink/data/McpAttemptEvent.java | 122 ++++ .../mcp/sink/data/McpConnectRecord.java | 118 ++++ .../mcp/sink/data/McpExportMetadata.java | 48 ++ .../mcp/sink/data/McpExportRecord.java | 37 ++ .../mcp/sink/data/McpExportRecordPage.java | 41 ++ .../mcp/sink/data/MultiMcpRequestContext.java | 72 ++ .../sink/handler/AbstractMcpSinkHandler.java | 79 +++ .../mcp/sink/handler/McpDeliveryStrategy.java | 23 + .../mcp/sink/handler/McpSinkHandler.java | 76 +++ .../handler/impl/CommonMcpSinkHandler.java | 268 ++++++++ .../impl/McpSinkHandlerRetryWrapper.java | 118 ++++ .../mcp/source/McpSourceConnector.java | 345 +++------- .../mcp/source/data/MCPStreamingResponse.java | 210 ------ .../connector/mcp/source/data/McpRequest.java | 75 +-- .../mcp/source/data/McpResponse.java | 51 +- .../mcp/source/data/McpStreamingRequest.java | 25 - .../handler/NettyMcpRequestHandler.java | 614 ------------------ .../mcp/source/protocol/McpConstants.java | 2 +- .../mcp/source/protocol/Protocol.java | 144 +--- .../mcp/source/protocol/ProtocolFactory.java | 17 + .../protocol/impl/McpStandardProtocol.java | 519 +++------------ .../connector/mcp/util/AIToolManager.java | 162 ----- .../mcp/util/CloudEventsExtensionUtil.java | 213 ------ .../mcp/util/McpResponseValidator.java | 311 --------- ...esh.openconnect.api.ConnectorCreateService | 1 + .../src/main/resources/server-config.yml | 2 +- .../src/main/resources/sink-config.yml | 56 ++ .../src/main/resources/source-config.yml | 13 +- .../demo/sub/RemoteSubscribeInstance.java | 44 +- .../eventmesh-protocol-mcp/gradle.porperties | 18 - .../protocol/mcp/McpProtocolAdaptor.java | 74 --- .../protocol/mcp/McpProtocolConstant.java | 45 -- .../resolver/McpRequestProtocolResolver.java | 98 --- ...che.eventmesh.protocol.mcp.ProtocolAdaptor | 16 - .../runtime/boot/AbstractMCPServer.java | 524 +++++++++++++++ .../runtime/boot/EventMeshMCPServer.java | 197 ++++++ .../runtime/boot/EventMeshMcpBootstrap.java | 51 ++ .../runtime/core/protocol/mcp/McpRetryer.java | 16 + 49 files changed, 2514 insertions(+), 2897 deletions(-) delete mode 100644 eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpMcpConfig.java create mode 100644 eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpRetryConfig.java rename eventmesh-protocol-plugin/eventmesh-protocol-mcp/build.gradle => eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpSinkConfig.java (64%) create mode 100644 eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SinkConnectorConfig.java delete mode 100644 eventmesh-common/src/main/java/org/apache/eventmesh/common/protocol/mcp/McpEventWrapper.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/MCPSession.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/SessionManager.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpAttemptEvent.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/MultiMcpRequestContext.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpDeliveryStrategy.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/MCPStreamingResponse.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpStreamingRequest.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/handler/NettyMcpRequestHandler.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/AIToolManager.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/CloudEventsExtensionUtil.java delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/McpResponseValidator.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/sink-config.yml delete mode 100644 eventmesh-protocol-plugin/eventmesh-protocol-mcp/gradle.porperties delete mode 100644 eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolAdaptor.java delete mode 100644 eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolConstant.java delete mode 100644 eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/resolver/McpRequestProtocolResolver.java delete mode 100644 eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/resources/eventmesh/org.apache.eventmesh.protocol.mcp.ProtocolAdaptor create mode 100644 eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/AbstractMCPServer.java create mode 100644 eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshMCPServer.java create mode 100644 eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshMcpBootstrap.java create mode 100644 eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/mcp/McpRetryer.java diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpMcpConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpMcpConfig.java deleted file mode 100644 index 5fc2f0b9d9..0000000000 --- a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/HttpMcpConfig.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.apache.eventmesh.common.config.connector.mcp; - -import lombok.Data; - -/** - * MCP Server 接入配置 - */ -@Data -public class HttpMcpConfig { - - /** - * 是否启用 MCP 服务 - */ - private boolean activate = false; - - /** - * MCP Server 监听端口 - */ - private int port; - - // Path to display/export callback data - private String exportPath = "/export"; - - /** - * Session 空闲超时时间(单位:毫秒) - */ - private int sessionIdleTimeoutMs = 300_000; - - /** - * 最大 Session 数(用于流式连接控制) - */ - private int maxConcurrentSessions = 500; - - /** - * 是否启用流式响应(Chunked Response) - */ - private boolean enableStreaming = true; - - /** - * 每个会话最大缓存消息数 - */ - private int maxBufferedMessagesPerSession = 100; - - /** - * 启动时是否打印 MCP 请求日志 - */ - private boolean enableDebugLog = false; -} diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpRetryConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpRetryConfig.java new file mode 100644 index 0000000000..44889f9ffa --- /dev/null +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpRetryConfig.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.common.config.connector.mcp; + +import lombok.Data; + +@Data +public class McpRetryConfig { + // maximum number of retries, default 2, minimum 0 + private int maxRetries = 2; + + // retry interval, default 1000ms + private int interval = 1000; + + // Default value is false, indicating that only requests with network-level errors will be retried. + // If set to true, all failed requests will be retried, including network-level errors and non-2xx responses. + private boolean retryOnNonSuccess = false; +} diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/build.gradle b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpSinkConfig.java similarity index 64% rename from eventmesh-protocol-plugin/eventmesh-protocol-mcp/build.gradle rename to eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpSinkConfig.java index d219c5dc03..93ecb78f10 100644 --- a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/build.gradle +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpSinkConfig.java @@ -15,13 +15,16 @@ * limitations under the License. */ -dependencies { - implementation project(":eventmesh-protocol-plugin:eventmesh-protocol-api") - implementation "io.cloudevents:cloudevents-core" - implementation "com.google.guava:guava" - implementation "io.cloudevents:cloudevents-json-jackson" - implementation ("io.grpc:grpc-protobuf:1.68.0") { - exclude group: "com.google.protobuf", module: "protobuf-java" - } - implementation("com.google.protobuf:protobuf-java:3.25.4") +package org.apache.eventmesh.common.config.connector.mcp; + +import org.apache.eventmesh.common.config.connector.SinkConfig; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class McpSinkConfig extends SinkConfig { + + public SinkConnectorConfig connectorConfig; } diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SinkConnectorConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SinkConnectorConfig.java new file mode 100644 index 0000000000..59df614ae6 --- /dev/null +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SinkConnectorConfig.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.common.config.connector.mcp; + + +import lombok.Data; +import org.apache.eventmesh.common.config.connector.mcp.McpRetryConfig; + +@Data +public class SinkConnectorConfig { + + private String connectorName; + + private String[] urls; + + // keepAlive, default true + private boolean keepAlive = true; + + // timeunit: ms, default 60000ms + private int keepAliveTimeout = 60 * 1000; // Keep units consistent + + // timeunit: ms, default 5000ms, recommended scope: 5000ms - 10000ms + private int connectionTimeout = 5000; + + // timeunit: ms, default 5000ms + private int idleTimeout = 5000; + + // maximum number of HTTP/1 connections a client will pool, default 50 + private int maxConnectionPoolSize = 50; + + // retry config + private McpRetryConfig retryConfig = new McpRetryConfig(); + + private String deliveryStrategy = "ROUND_ROBIN"; + + private boolean skipDeliverException = false; + + // managed pipelining param, default true + private boolean isParallelized = true; + + private int parallelism = 2; + + + /** + * Fill default values if absent (When there are multiple default values for a field) + * + * @param config SinkConnectorConfig + */ + public static void populateFieldsWithDefaults(SinkConnectorConfig config) { + /* + * set default values for idleTimeout + * recommended scope: common(5s - 10s), webhook(15s - 30s) + */ + final int commonHttpIdleTimeout = 5000; + + // Set default values for idleTimeout + if (config.getIdleTimeout() == 0) { + config.setIdleTimeout(commonHttpIdleTimeout); + } + + } +} diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/protocol/mcp/McpEventWrapper.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/protocol/mcp/McpEventWrapper.java deleted file mode 100644 index 70b6bdd40b..0000000000 --- a/eventmesh-common/src/main/java/org/apache/eventmesh/common/protocol/mcp/McpEventWrapper.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.apache.eventmesh.common.protocol.mcp; - -import org.apache.eventmesh.common.protocol.ProtocolTransportObject; -import org.apache.eventmesh.common.protocol.http.common.ProtocolKey; -import org.apache.commons.lang3.StringUtils; - -import java.net.URI; -import java.util.*; - -public class McpEventWrapper implements ProtocolTransportObject { - - private transient Map headerMap = new HashMap<>(); - - private transient Map sysHeaderMap = new HashMap<>(); - - private byte[] body; - - private long reqTime; - - private long resTime; - - public McpEventWrapper() { - this.reqTime = System.currentTimeMillis(); - } - - public Map getHeaderMap() { - return headerMap; - } - - public void setHeaderMap(Map headerMap) { - this.headerMap = headerMap; - } - - public Map getSysHeaderMap() { - return sysHeaderMap; - } - - public void setSysHeaderMap(Map sysHeaderMap) { - this.sysHeaderMap = sysHeaderMap; - } - - public byte[] getBody() { - int len = body.length; - byte[] b = new byte[len]; - System.arraycopy(body, 0, b, 0, len); - return b; - } - - public void setBody(byte[] newBody) { - if (newBody == null || newBody.length == 0) { - return; - } - this.body = Arrays.copyOf(newBody, newBody.length); - } - - public long getReqTime() { - return reqTime; - } - - public void setReqTime(long reqTime) { - this.reqTime = reqTime; - } - - public long getResTime() { - return resTime; - } - - public void setResTime(long resTime) { - this.resTime = resTime; - } - - public void buildSysHeaderForClient() { - sysHeaderMap.put(ProtocolKey.PROTOCOL_TYPE, "mcp"); - sysHeaderMap.put(ProtocolKey.PROTOCOL_DESC, "mcp"); - - for (ProtocolKey.ClientInstanceKey key : ProtocolKey.ClientInstanceKey.values()) { - if (key == ProtocolKey.ClientInstanceKey.BIZSEQNO || key == ProtocolKey.ClientInstanceKey.UNIQUEID) { - continue; - } - sysHeaderMap.put(key.getKey(), headerMap.getOrDefault(key.getKey(), key.getValue())); - } - } - - public void buildSysHeaderForCE() { - sysHeaderMap.put(ProtocolKey.CloudEventsKey.ID, UUID.randomUUID().toString()); - sysHeaderMap.put(ProtocolKey.CloudEventsKey.SOURCE, headerMap.getOrDefault("source", URI.create("/"))); - sysHeaderMap.put(ProtocolKey.CloudEventsKey.TYPE, headerMap.getOrDefault("type", "mcp_request")); - - String topic = headerMap.getOrDefault("subject", "").toString(); - if (StringUtils.isEmpty(topic)) { - topic = "TEST-MCP-TOPIC"; - } - sysHeaderMap.put(ProtocolKey.CloudEventsKey.SUBJECT, topic); - } -} diff --git a/eventmesh-connectors/eventmesh-connector-http/src/main/resources/server-config.yml b/eventmesh-connectors/eventmesh-connector-http/src/main/resources/server-config.yml index 0cd7b5b5ab..5f66dd0f68 100644 --- a/eventmesh-connectors/eventmesh-connector-http/src/main/resources/server-config.yml +++ b/eventmesh-connectors/eventmesh-connector-http/src/main/resources/server-config.yml @@ -16,4 +16,4 @@ # sourceEnable: true -sinkEnable: false +sinkEnable: true diff --git a/eventmesh-connectors/eventmesh-connector-mcp/build.gradle b/eventmesh-connectors/eventmesh-connector-mcp/build.gradle index ff9d734208..82072c2876 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/build.gradle +++ b/eventmesh-connectors/eventmesh-connector-mcp/build.gradle @@ -18,7 +18,7 @@ dependencies { api project(":eventmesh-openconnect:eventmesh-openconnect-java") implementation project(":eventmesh-common") - + implementation project(":eventmesh-connectors:eventmesh-connector-http") implementation project(":eventmesh-protocol-plugin:eventmesh-protocol-api") implementation "io.cloudevents:cloudevents-core" implementation "com.google.guava:guava" @@ -35,12 +35,6 @@ dependencies { testImplementation 'org.apache.httpcomponents.client5:httpclient5:5.4' testImplementation 'org.apache.httpcomponents.client5:httpclient5-fluent:5.4' testImplementation 'org.mock-server:mockserver-netty:5.15.0' - - implementation("com.google.protobuf:protobuf-java:3.25.4") - implementation 'io.netty:netty-common:4.1.114.Final' - implementation 'io.netty:netty-buffer:4.1.114.Final' - implementation 'io.netty:netty-transport:4.1.114.Final' - implementation 'io.netty:netty-handler:4.1.114.Final' implementation 'io.netty:netty-codec-http:4.1.114.Final' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java index d3eb1e733d..8f6ef0305f 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java @@ -1,6 +1,7 @@ package org.apache.eventmesh.connector.mcp.server; import org.apache.eventmesh.connector.mcp.config.McpServerConfig; +import org.apache.eventmesh.connector.mcp.sink.McpSinkConnector; import org.apache.eventmesh.connector.mcp.source.McpSourceConnector; import org.apache.eventmesh.openconnect.Application; import org.apache.eventmesh.openconnect.util.ConfigUtil; @@ -13,5 +14,10 @@ public static void main(String[] args) throws Exception { Application mcpSourceApp = new Application(); mcpSourceApp.run(McpSourceConnector.class); } + + if (serverConfig.isSinkEnable()) { + Application mcpSinkApp = new Application(); + mcpSinkApp.run(McpSinkConnector.class); + } } } diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/MCPSession.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/MCPSession.java deleted file mode 100644 index 821e138249..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/MCPSession.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.apache.eventmesh.connector.mcp.session; - -import lombok.Getter; - -import java.util.concurrent.ConcurrentHashMap; - -public class MCPSession { - @Getter - private final String sessionId; - private final ConcurrentHashMap context = new ConcurrentHashMap<>(); - @Getter - private volatile long lastActivity; - - public MCPSession(String sessionId) { - this.sessionId = sessionId; - this.lastActivity = System.currentTimeMillis(); - } - - public void updateLastActivity() { - this.lastActivity = System.currentTimeMillis(); - } - - public void setContext(String key, Object value) { - context.put(key, value); - } - - public Object getContext(String key) { - return context.get(key); - } - - public void clearContext() { - context.clear(); - } -} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/SessionManager.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/SessionManager.java deleted file mode 100644 index a34da41f49..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/session/SessionManager.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.apache.eventmesh.connector.mcp.session; - -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -public class SessionManager { - private final ConcurrentHashMap sessions = new ConcurrentHashMap<>(); - private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); - - public SessionManager() { - // 定时清理过期会话 - scheduler.scheduleAtFixedRate(this::cleanupExpiredSessions, 5, 5, TimeUnit.MINUTES); - } - - public MCPSession createSession() { - String sessionId = UUID.randomUUID().toString(); - MCPSession session = new MCPSession(sessionId); - sessions.put(sessionId, session); - return session; - } - - public MCPSession createSessionWithId(String sessionId) { - if (sessionId == null || sessionId.trim().isEmpty()) { - return createSession(); - } - - // 检查会话ID是否已存在 - if (sessions.containsKey(sessionId)) { - return sessions.get(sessionId); - } - - MCPSession session = new MCPSession(sessionId); - sessions.put(sessionId, session); - return session; - } - - public void putSession(MCPSession session) { - if (session != null) { - sessions.put(session.getSessionId(), session); - } - } - - public MCPSession getSession(String sessionId) { - MCPSession session = sessions.get(sessionId); - if (session != null) { - session.updateLastActivity(); - } - return session; - } - - public void removeSession(String sessionId) { - sessions.remove(sessionId); - } - - public void cleanupExpiredSessions(long currentTime) { - sessions.entrySet().removeIf(entry -> { - MCPSession session = entry.getValue(); - return currentTime - session.getLastActivity() > 30 * 60 * 1000; // 30分钟超时 - }); - } - - private void cleanupExpiredSessions() { - cleanupExpiredSessions(System.currentTimeMillis()); - } - - public void shutdown() { - scheduler.shutdown(); - } -} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java new file mode 100644 index 0000000000..8d97da2a0c --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink; + +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.eventmesh.common.EventMeshThreadFactory; +import org.apache.eventmesh.common.config.connector.Config; +import org.apache.eventmesh.common.config.connector.mcp.SinkConnectorConfig; +import org.apache.eventmesh.common.config.connector.mcp.McpSinkConfig; +import org.apache.eventmesh.connector.mcp.sink.handler.McpSinkHandler; +import org.apache.eventmesh.connector.mcp.sink.handler.impl.CommonMcpSinkHandler; +import org.apache.eventmesh.connector.mcp.sink.handler.impl.McpSinkHandlerRetryWrapper; +import org.apache.eventmesh.openconnect.api.ConnectorCreateService; +import org.apache.eventmesh.openconnect.api.connector.ConnectorContext; +import org.apache.eventmesh.openconnect.api.connector.SinkConnectorContext; +import org.apache.eventmesh.openconnect.api.sink.Sink; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +@Slf4j +public class McpSinkConnector implements Sink, ConnectorCreateService { + + private McpSinkConfig mcpSinkConfig; + + @Getter + private McpSinkHandler sinkHandler; + + private ThreadPoolExecutor executor; + + private final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(10000); + + private final AtomicBoolean isStart = new AtomicBoolean(true); + + @Override + public Class configClass() { + return McpSinkConfig.class; + } + + @Override + public Sink create() { + return new McpSinkConnector(); + } + + @Override + public void init(Config config) throws Exception { + this.mcpSinkConfig = (McpSinkConfig) config; + doInit(); + } + + @Override + public void init(ConnectorContext connectorContext) throws Exception { + SinkConnectorContext sinkConnectorContext = (SinkConnectorContext) connectorContext; + this.mcpSinkConfig = (McpSinkConfig) sinkConnectorContext.getSinkConfig(); + doInit(); + } + + @SneakyThrows + private void doInit() { + // Fill default values if absent + SinkConnectorConfig.populateFieldsWithDefaults(this.mcpSinkConfig.connectorConfig); + // Create different handlers for different configurations + McpSinkHandler nonRetryHandler; + + nonRetryHandler = new CommonMcpSinkHandler(this.mcpSinkConfig.connectorConfig); + + int maxRetries = this.mcpSinkConfig.connectorConfig.getRetryConfig().getMaxRetries(); + if (maxRetries == 0) { + // Use the original sink handler + this.sinkHandler = nonRetryHandler; + } else if (maxRetries > 0) { + // Wrap the sink handler with a retry handler + this.sinkHandler = new McpSinkHandlerRetryWrapper(this.mcpSinkConfig.connectorConfig, nonRetryHandler); + } else { + throw new IllegalArgumentException("Max retries must be greater than or equal to 0."); + } + boolean isParallelized = this.mcpSinkConfig.connectorConfig.isParallelized(); + int parallelism = isParallelized ? this.mcpSinkConfig.connectorConfig.getParallelism() : 1; + executor = new ThreadPoolExecutor(parallelism, parallelism, 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(), new EventMeshThreadFactory("mcp-sink-handler")); + } + + @Override + public void start() throws Exception { + this.sinkHandler.start(); + for (int i = 0; i < this.mcpSinkConfig.connectorConfig.getParallelism(); i++) { + executor.execute(() -> { + while (isStart.get()) { + ConnectRecord connectRecord = null; + try { + connectRecord = queue.poll(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (connectRecord != null) { + sinkHandler.handle(connectRecord); + } + } + }); + } + } + + @Override + public void commit(ConnectRecord record) { + + } + + @Override + public String name() { + return this.mcpSinkConfig.connectorConfig.getConnectorName(); + } + + @Override + public void onException(ConnectRecord record) { + + } + + @Override + public void stop() throws Exception { + isStart.set(false); + while (!queue.isEmpty()) { + ConnectRecord record = queue.poll(); + this.sinkHandler.handle(record); + } + try { + Thread.sleep(50); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + this.sinkHandler.stop(); + log.info("All tasks completed, start shut down mcp sink connector"); + } + + @Override + public void put(List sinkRecords) { + for (ConnectRecord sinkRecord : sinkRecords) { + try { + if (Objects.isNull(sinkRecord)) { + log.warn("ConnectRecord data is null, ignore."); + continue; + } + queue.put(sinkRecord); + } catch (Exception e) { + log.error("Failed to sink message via mcp. ", e); + } + } + } +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpAttemptEvent.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpAttemptEvent.java new file mode 100644 index 0000000000..451fc3523d --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpAttemptEvent.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Single MCP attempt event + */ +public class McpAttemptEvent { + + public static final String PREFIX = "mcp-attempt-event-"; + + private final int maxAttempts; + + private final AtomicInteger attempts; + + private Throwable lastException; + + + public McpAttemptEvent(int maxAttempts) { + this.maxAttempts = maxAttempts; + this.attempts = new AtomicInteger(0); + } + + /** + * Increment the attempts + */ + public void incrementAttempts() { + attempts.incrementAndGet(); + } + + /** + * Update the event, incrementing the attempts and setting the last exception + * + * @param exception the exception to update, can be null + */ + public void updateEvent(Throwable exception) { + // increment the attempts + incrementAttempts(); + + // update the last exception + lastException = exception; + } + + /** + * Check if the attempts are less than the maximum attempts + * + * @return true if the attempts are less than the maximum attempts, false otherwise + */ + public boolean canAttempt() { + return attempts.get() < maxAttempts; + } + + public boolean isComplete() { + if (attempts.get() == 0) { + // No start yet + return false; + } + + // If no attempt can be made or the last exception is null, the event completed + return !canAttempt() || lastException == null; + } + + + public int getMaxAttempts() { + return maxAttempts; + } + + public int getAttempts() { + return attempts.get(); + } + + public Throwable getLastException() { + return lastException; + } + + /** + * Get the limited exception message with the default limit of 256 + * + * @return the limited exception message + */ + public String getLimitedExceptionMessage() { + return getLimitedExceptionMessage(256); + } + + /** + * Get the limited exception message with the specified limit + * + * @param maxLimit the maximum limit of the exception message + * @return the limited exception message + */ + public String getLimitedExceptionMessage(int maxLimit) { + if (lastException == null) { + return ""; + } + String message = lastException.getMessage(); + if (message == null) { + return ""; + } + if (message.length() > maxLimit) { + return message.substring(0, maxLimit); + } + return message; + } + +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java new file mode 100644 index 0000000000..990b39b4f6 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; + +import org.apache.eventmesh.common.remote.offset.http.HttpRecordOffset; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.KeyValue; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import lombok.Builder; +import lombok.Getter; + +/** + * a special ConnectRecord for HttpSinkConnector + */ +@Getter +@Builder +public class McpConnectRecord implements Serializable { + + private static final long serialVersionUID = 5271462532332251473L; + + /** + * The unique identifier for the HttpConnectRecord + */ + private final String httpRecordId = UUID.randomUUID().toString(); + + /** + * The time when the HttpConnectRecord was created + */ + private LocalDateTime createTime; + + /** + * The type of the HttpConnectRecord + */ + private String type; + + /** + * The event id of the HttpConnectRecord + */ + private String eventId; + + private Object data; + + private KeyValue extensions; + + @Override + public String toString() { + return "HttpConnectRecord{" + + "createTime=" + createTime + + ", httpRecordId='" + httpRecordId + + ", type='" + type + + ", eventId='" + eventId + + ", data=" + data + + ", extensions=" + extensions + + '}'; + } + + /** + * Convert ConnectRecord to HttpConnectRecord + * + * @param record the ConnectRecord to convert + * @return the converted HttpConnectRecord + */ + public static McpConnectRecord convertConnectRecord(ConnectRecord record, String type) { + Map offsetMap = new HashMap<>(); + if (record != null && record.getPosition() != null && record.getPosition().getRecordOffset() != null) { + if (HttpRecordOffset.class.equals(record.getPosition().getRecordOffsetClazz())) { + offsetMap = ((HttpRecordOffset) record.getPosition().getRecordOffset()).getOffsetMap(); + } + } + String offset = "0"; + if (!offsetMap.isEmpty()) { + offset = offsetMap.values().iterator().next().toString(); + } + if (record.getData() instanceof byte[]) { + String data = Base64.getEncoder().encodeToString((byte[]) record.getData()); + record.addExtension("isBase64", true); + return McpConnectRecord.builder() + .type(type) + .createTime(LocalDateTime.now()) + .eventId(type + "-" + offset) + .data(data) + .extensions(record.getExtensions()) + .build(); + } else { + record.addExtension("isBase64", false); + return McpConnectRecord.builder() + .type(type) + .createTime(LocalDateTime.now()) + .eventId(type + "-" + offset) + .data(record.getData()) + .extensions(record.getExtensions()) + .build(); + } + } + +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java new file mode 100644 index 0000000000..72595b2637 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +import lombok.Builder; +import lombok.Data; + +/** + * Metadata for an MCP export operation. + */ +@Data +@Builder +public class McpExportMetadata implements Serializable { + + private static final long serialVersionUID = 1121010466793041920L; + + private String url; + + private int code; + + private String message; + + private LocalDateTime receivedTime; + + private String recordId; + + private String retriedBy; + + private int retryNum; +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java new file mode 100644 index 0000000000..c9a35c193b --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; + +import java.io.Serializable; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Represents an MCP export record containing metadata and data to be exported. + */ +@Data +@AllArgsConstructor +public class McpExportRecord implements Serializable { + + private static final long serialVersionUID = 6010283911452947157L; + + private McpExportMetadata metadata; + + private Object data; +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java new file mode 100644 index 0000000000..3e0e615523 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; + +import java.io.Serializable; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Represents a page of MCP export records. + */ +@Data +@AllArgsConstructor +public class McpExportRecordPage implements Serializable { + + private static final long serialVersionUID = 1143791658357035990L; + + private int pageNum; + + private int pageSize; + + private List pageItems; + +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/MultiMcpRequestContext.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/MultiMcpRequestContext.java new file mode 100644 index 0000000000..f24d0e3fd1 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/MultiMcpRequestContext.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.data; + +import java.util.concurrent.atomic.AtomicInteger; + + +/** + * Multi Mcp request context + */ +public class MultiMcpRequestContext { + + public static final String NAME = "multi-http-request-context"; + + /** + * The remaining requests to be processed. + */ + private final AtomicInteger remainingRequests; + + /** + * The last failed event. + * If retries occur but still fail, it will be logged, and only the last one will be retained. + */ + private McpAttemptEvent lastFailedEvent; + + public MultiMcpRequestContext(int remainingEvents) { + this.remainingRequests = new AtomicInteger(remainingEvents); + } + + /** + * Decrement the remaining requests by 1. + */ + public void decrementRemainingRequests() { + remainingRequests.decrementAndGet(); + } + + /** + * Check if all requests have been processed. + * + * @return true if all requests have been processed, false otherwise. + */ + public boolean isAllRequestsProcessed() { + return remainingRequests.get() == 0; + } + + public int getRemainingRequests() { + return remainingRequests.get(); + } + + public McpAttemptEvent getLastFailedEvent() { + return lastFailedEvent; + } + + public void setLastFailedEvent(McpAttemptEvent lastFailedEvent) { + this.lastFailedEvent = lastFailedEvent; + } +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java new file mode 100644 index 0000000000..f7b16aa40d --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java @@ -0,0 +1,79 @@ +package org.apache.eventmesh.connector.mcp.sink.handler; + +import lombok.Getter; +import org.apache.eventmesh.common.config.connector.mcp.SinkConnectorConfig; +import org.apache.eventmesh.connector.mcp.sink.data.McpAttemptEvent; +import org.apache.eventmesh.connector.mcp.sink.data.McpConnectRecord; +import org.apache.eventmesh.connector.mcp.sink.data.MultiMcpRequestContext; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public abstract class AbstractMcpSinkHandler implements McpSinkHandler { + @Getter + private final SinkConnectorConfig sinkConnectorConfig; + + @Getter + private final List urls; + + private final McpDeliveryStrategy deliveryStrategy; + + private int roundRobinIndex = 0; + + protected AbstractMcpSinkHandler(SinkConnectorConfig sinkConnectorConfig) { + this.sinkConnectorConfig = sinkConnectorConfig; + this.deliveryStrategy = McpDeliveryStrategy.valueOf(sinkConnectorConfig.getDeliveryStrategy()); + // Initialize URLs + String[] urlStrings = sinkConnectorConfig.getUrls(); + this.urls = Arrays.stream(urlStrings) + .map(URI::create) + .collect(Collectors.toList()); + } + + /** + * Processes a ConnectRecord by sending it over HTTP or HTTPS. This method should be called for each ConnectRecord that needs to be processed. + * + * @param record the ConnectRecord to process + */ + @Override + public void handle(ConnectRecord record) { + // build attributes + Map attributes = new ConcurrentHashMap<>(); + + switch (deliveryStrategy) { + case ROUND_ROBIN: + attributes.put(MultiMcpRequestContext.NAME, new MultiMcpRequestContext(1)); + URI url = urls.get(roundRobinIndex); + roundRobinIndex = (roundRobinIndex + 1) % urls.size(); + sendRecordToUrl(record, attributes, url); + break; + case BROADCAST: + attributes.put(MultiMcpRequestContext.NAME, new MultiMcpRequestContext(urls.size())); + // send the record to all URLs + urls.forEach(url0 -> sendRecordToUrl(record, attributes, url0)); + break; + default: + throw new IllegalArgumentException("Unknown delivery strategy: " + deliveryStrategy); + } + } + + private void sendRecordToUrl(ConnectRecord record, Map attributes, URI url) { + // convert ConnectRecord to HttpConnectRecord + String type = String.format("%s.%s.%s", + this.sinkConnectorConfig.getConnectorName(), url.getScheme(), + "common"); + McpConnectRecord mcpConnectRecord = McpConnectRecord.convertConnectRecord(record, type); + + // add AttemptEvent to the attributes + McpAttemptEvent attemptEvent = new McpAttemptEvent(this.sinkConnectorConfig.getRetryConfig().getMaxRetries() + 1); + attributes.put(McpAttemptEvent.PREFIX + mcpConnectRecord.getHttpRecordId(), attemptEvent); + + // deliver the record + deliver(url, mcpConnectRecord, attributes, record); + } +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpDeliveryStrategy.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpDeliveryStrategy.java new file mode 100644 index 0000000000..07cbbe3d46 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpDeliveryStrategy.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.handler; + +public enum McpDeliveryStrategy { + ROUND_ROBIN, + BROADCAST +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java new file mode 100644 index 0000000000..c10d96337b --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.handler; + +import org.apache.eventmesh.connector.mcp.sink.data.McpConnectRecord; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; + +import java.net.URI; +import java.util.Map; + +import io.vertx.core.Future; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.client.HttpResponse; + +/** + * Interface for handling ConnectRecords via HTTP or HTTPS. Classes implementing this interface are responsible for processing ConnectRecords by + * sending them over HTTP or HTTPS, with additional support for handling multiple requests and asynchronous processing. + * + *

Any class that needs to process ConnectRecords via HTTP or HTTPS should implement this interface. + * Implementing classes must provide implementations for the {@link #start()}, {@link #handle(ConnectRecord)}, + * {@link #deliver(URI, McpConnectRecord, Map, ConnectRecord)}, and {@link #stop()} methods.

+ * + *

Implementing classes should ensure thread safety and handle MCP communication efficiently. + * The {@link #start()} method initializes any necessary resources for MCP communication. The {@link #handle(ConnectRecord)} method processes a + * ConnectRecord by sending it over HTTP or HTTPS. The {@link #deliver(URI, McpConnectRecord, Map, ConnectRecord)} method processes HttpConnectRecord + * on specified URL while returning its own processing logic {@link #stop()} method releases any resources used for MCP communication.

+ * + *

It's recommended to handle exceptions gracefully within the {@link #deliver(URI, McpConnectRecord, Map, ConnectRecord)} method + * to prevent message loss or processing interruptions.

+ */ +public interface McpSinkHandler { + + /** + * Initializes the MCP handler. This method should be called before using the handler. + */ + void start(); + + /** + * Processes a ConnectRecord by sending it over HTTP or HTTPS. This method should be called for each ConnectRecord that needs to be processed. + * + * @param record the ConnectRecord to process + */ + void handle(ConnectRecord record); + + + /** + * Processes HttpConnectRecord on specified URL while returning its own processing logic + * + * @param url URI to which the HttpConnectRecord should be sent + * @param mcpConnectRecord HttpConnectRecord to process + * @param attributes additional attributes to be used in processing + * @return processing chain + */ + Future> deliver(URI url, McpConnectRecord mcpConnectRecord, Map attributes, ConnectRecord connectRecord); + + /** + * Cleans up and releases resources used by the MCP handler. This method should be called when the handler is no longer needed. + */ + void stop(); +} + diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java new file mode 100644 index 0000000000..01cb9c2d08 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java @@ -0,0 +1,268 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.handler.impl; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpHeaders; +import io.vertx.ext.web.client.HttpResponse; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.eventmesh.common.config.connector.mcp.SinkConnectorConfig; +import org.apache.eventmesh.common.utils.JsonUtils; +import org.apache.eventmesh.connector.mcp.sink.data.McpAttemptEvent; +import org.apache.eventmesh.connector.mcp.sink.data.McpConnectRecord; +import org.apache.eventmesh.connector.http.util.HttpUtils; +import org.apache.eventmesh.connector.mcp.sink.data.MultiMcpRequestContext; +import org.apache.eventmesh.connector.mcp.sink.handler.AbstractMcpSinkHandler; +import org.apache.eventmesh.openconnect.offsetmgmt.api.callback.SendExceptionContext; +import org.apache.eventmesh.openconnect.offsetmgmt.api.callback.SendResult; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; + +import java.net.URI; +import java.time.ZoneId; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Common MCP Sink Handler implementation to handle ConnectRecords by sending them over MCP to configured URLs. + * + *

This handler initializes a WebClient for making HTTP requests based on the provided SinkConnectorConfig. + * It handles processing ConnectRecords by converting them to HttpConnectRecord and sending them asynchronously to each configured URL using the + * WebClient.

+ * + *

The handler uses Vert.x's WebClient to perform HTTP/HTTPS requests. It initializes the WebClient in the {@link #start()} + * method and closes it in the {@link #stop()} method to manage resources efficiently.

+ * + *

Each ConnectRecord is processed and sent to all configured URLs concurrently using asynchronous HTTP requests.

+ */ +@Slf4j +@Getter +public class CommonMcpSinkHandler extends AbstractMcpSinkHandler { + + private WebClient webClient; + + + public CommonMcpSinkHandler(SinkConnectorConfig sinkConnectorConfig) { + super(sinkConnectorConfig); + } + + /** + * Initializes the WebClient for making HTTP requests based on the provided SinkConnectorConfig. + */ + @Override + public void start() { + // Create WebClient + doInitWebClient(); + } + + /** + * Initializes the WebClient with the provided configuration options. + */ + private void doInitWebClient() { + SinkConnectorConfig sinkConnectorConfig = getSinkConnectorConfig(); + final Vertx vertx = Vertx.vertx(); + WebClientOptions options = new WebClientOptions() + .setKeepAlive(sinkConnectorConfig.isKeepAlive()) + .setKeepAliveTimeout(sinkConnectorConfig.getKeepAliveTimeout() / 1000) + .setIdleTimeout(sinkConnectorConfig.getIdleTimeout()) + .setIdleTimeoutUnit(TimeUnit.MILLISECONDS) + .setConnectTimeout(sinkConnectorConfig.getConnectionTimeout()) + .setMaxPoolSize(sinkConnectorConfig.getMaxConnectionPoolSize()) + .setPipelining(sinkConnectorConfig.isParallelized()); + this.webClient = WebClient.create(vertx, options); + } + + /** + * Processes HttpConnectRecord on specified URL while returning its own processing logic. This method sends the HttpConnectRecord to the specified + * URL using the WebClient. + * + * @param url URI to which the HttpConnectRecord should be sent + * @param mcpConnectRecord HttpConnectRecord to process + * @param attributes additional attributes to be used in processing + * @return processing chain + */ + @Override + public Future> deliver(URI url, McpConnectRecord mcpConnectRecord, Map attributes, + ConnectRecord connectRecord) { + // create headers + Map extensionMap = new HashMap<>(); + Set extensionKeySet = mcpConnectRecord.getExtensions().keySet(); + for (String extensionKey : extensionKeySet) { + Object v = mcpConnectRecord.getExtensions().getObject(extensionKey); + extensionMap.put(extensionKey, v); + } + + MultiMap headers = HttpHeaders.headers() + .set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8") + .set(HttpHeaderNames.ACCEPT, "application/json; charset=utf-8") + .set("extension", JsonUtils.toJSONString(extensionMap)); + // get timestamp and offset + Long timestamp = mcpConnectRecord.getCreateTime() + .atZone(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli(); + + // send the request + return this.webClient.post(url.getPath()) + .host(url.getHost()) + .port(url.getPort() == -1 ? (Objects.equals(url.getScheme(), "https") ? 443 : 80) : url.getPort()) + .putHeaders(headers) + .ssl(Objects.equals(url.getScheme(), "https")) + .sendJson(mcpConnectRecord.getData()) + .onSuccess(res -> { + log.info("Request sent successfully. Record: timestamp={}", timestamp); + + Exception e = null; + + // log the response + if (HttpUtils.is2xxSuccessful(res.statusCode())) { + if (log.isDebugEnabled()) { + log.debug("Received successful response: statusCode={}. Record: timestamp={}, responseBody={}", + res.statusCode(), timestamp, res.bodyAsString()); + } else { + log.info("Received successful response: statusCode={}. Record: timestamp={}", res.statusCode(), timestamp); + } + } else { + if (log.isDebugEnabled()) { + log.warn("Received non-2xx response: statusCode={}. Record: timestamp={}, responseBody={}", + res.statusCode(), timestamp, res.bodyAsString()); + } else { + log.warn("Received non-2xx response: statusCode={}. Record: timestamp={}", res.statusCode(), timestamp); + } + + e = new RuntimeException("Unexpected HTTP response code: " + res.statusCode()); + } + + // try callback + tryCallback(mcpConnectRecord, e, attributes, connectRecord); + }).onFailure(err -> { + log.error("Request failed to send. Record: timestamp={}", timestamp, err); + + // try callback + tryCallback(mcpConnectRecord, err, attributes, connectRecord); + }); + } + + /** + * Tries to call the callback based on the result of the request. + * + * @param mcpConnectRecord the McpConnectRecord to use + * @param e the exception thrown during the request, may be null + * @param attributes additional attributes to be used in processing + */ + private void tryCallback(McpConnectRecord mcpConnectRecord, Throwable e, Map attributes, ConnectRecord record) { + // get and update the attempt event + McpAttemptEvent attemptEvent = (McpAttemptEvent) attributes.get(McpAttemptEvent.PREFIX + mcpConnectRecord.getHttpRecordId()); + attemptEvent.updateEvent(e); + + // get and update the multiHttpRequestContext + MultiMcpRequestContext multiMcpRequestContext = getAndUpdateMultiMcpRequestContext(attributes, attemptEvent); + + if (multiMcpRequestContext.isAllRequestsProcessed()) { + // do callback + if (record.getCallback() == null) { + if (log.isDebugEnabled()) { + log.warn("ConnectRecord callback is null. Ignoring callback. {}", record); + } else { + log.warn("ConnectRecord callback is null. Ignoring callback."); + } + return; + } + + // get the last failed event + McpAttemptEvent lastFailedEvent = multiMcpRequestContext.getLastFailedEvent(); + if (lastFailedEvent == null) { + // success + record.getCallback().onSuccess(convertToSendResult(record)); + } else { + // failure + record.getCallback().onException(buildSendExceptionContext(record, lastFailedEvent.getLastException())); + } + } else { + log.warn("still have requests to process, size {}|attempt num {}", + multiMcpRequestContext.getRemainingRequests(), attemptEvent.getAttempts()); + } + } + + + /** + * Gets and updates the multi mcp request context based on the provided attributes and HttpConnectRecord. + * + * @param attributes the attributes to use + * @param attemptEvent the McpAttemptEvent to use + * @return the updated multi mcp request context + */ + private MultiMcpRequestContext getAndUpdateMultiMcpRequestContext(Map attributes, McpAttemptEvent attemptEvent) { + // get the multi http request context + MultiMcpRequestContext multiMcpRequestContext = (MultiMcpRequestContext) attributes.get(MultiMcpRequestContext.NAME); + + // Check if the current attempted event has completed + if (attemptEvent.isComplete()) { + // decrement the counter + multiMcpRequestContext.decrementRemainingRequests(); + + if (attemptEvent.getLastException() != null) { + // if all attempts are exhausted, set the last failed event + multiMcpRequestContext.setLastFailedEvent(attemptEvent); + } + } + + return multiMcpRequestContext; + } + + private SendResult convertToSendResult(ConnectRecord record) { + SendResult result = new SendResult(); + result.setMessageId(record.getRecordId()); + if (org.apache.commons.lang3.StringUtils.isNotEmpty(record.getExtension("topic"))) { + result.setTopic(record.getExtension("topic")); + } + return result; + } + + private SendExceptionContext buildSendExceptionContext(ConnectRecord record, Throwable e) { + SendExceptionContext sendExceptionContext = new SendExceptionContext(); + sendExceptionContext.setMessageId(record.getRecordId()); + sendExceptionContext.setCause(e); + if (org.apache.commons.lang3.StringUtils.isNotEmpty(record.getExtension("topic"))) { + sendExceptionContext.setTopic(record.getExtension("topic")); + } + return sendExceptionContext; + } + + + /** + * Cleans up and releases resources used by the MCP handler. + */ + @Override + public void stop() { + if (this.webClient != null) { + this.webClient.close(); + } else { + log.warn("WebClient is null, ignore."); + } + } +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java new file mode 100644 index 0000000000..ae3b9f37d8 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * Mcp://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.apache.eventmesh.connector.mcp.sink.handler.impl; + +import dev.failsafe.Failsafe; +import dev.failsafe.RetryPolicy; +import io.vertx.core.Future; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.client.HttpResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.eventmesh.common.config.connector.mcp.McpRetryConfig; +import org.apache.eventmesh.common.config.connector.mcp.SinkConnectorConfig; +import org.apache.eventmesh.connector.http.util.HttpUtils; +import org.apache.eventmesh.connector.mcp.sink.data.McpConnectRecord; +import org.apache.eventmesh.connector.mcp.sink.handler.AbstractMcpSinkHandler; +import org.apache.eventmesh.connector.mcp.sink.handler.McpSinkHandler; +import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; + +import java.net.ConnectException; +import java.net.URI; +import java.time.Duration; +import java.util.Map; + + +/** + * McpSinkHandlerRetryWrapper is a wrapper class for the McpSinkHandler that provides retry functionality for failed Mcp requests. + */ +@Slf4j +public class McpSinkHandlerRetryWrapper extends AbstractMcpSinkHandler { + + private final McpRetryConfig mcpRetryConfig; + + private final McpSinkHandler sinkHandler; + + private final RetryPolicy> retryPolicy; + + public McpSinkHandlerRetryWrapper(SinkConnectorConfig sinkConnectorConfig, McpSinkHandler sinkHandler) { + super(sinkConnectorConfig); + this.sinkHandler = sinkHandler; + this.mcpRetryConfig = getSinkConnectorConfig().getRetryConfig(); + this.retryPolicy = buildRetryPolicy(); + } + + private RetryPolicy> buildRetryPolicy() { + return RetryPolicy.>builder() + .handleIf(e -> e instanceof ConnectException) + .handleResultIf(response -> mcpRetryConfig.isRetryOnNonSuccess() && !HttpUtils.is2xxSuccessful(response.statusCode())) + .withMaxRetries(mcpRetryConfig.getMaxRetries()) + .withDelay(Duration.ofMillis(mcpRetryConfig.getInterval())) + .onRetry(event -> { + if (log.isDebugEnabled()) { + log.warn("Failed to deliver message after {} attempts. Retrying in {} ms. Error: {}", + event.getAttemptCount(), mcpRetryConfig.getInterval(), event.getLastException()); + } else { + log.warn("Failed to deliver message after {} attempts. Retrying in {} ms.", + event.getAttemptCount(), mcpRetryConfig.getInterval()); + } + }).onFailure(event -> { + if (log.isDebugEnabled()) { + log.error("Failed to deliver message after {} attempts. Error: {}", + event.getAttemptCount(), event.getException()); + } else { + log.error("Failed to deliver message after {} attempts.", + event.getAttemptCount()); + } + }).build(); + } + + /** + * Initializes the WebClient for making Mcp requests based on the provided SinkConnectorConfig. + */ + @Override + public void start() { + sinkHandler.start(); + } + + + /** + * Processes McpConnectRecord on specified URL while returning its own processing logic This method provides the retry power to process the + * McpConnectRecord + * + * @param url URI to which the McpConnectRecord should be sent + * @param mcpConnectRecord McpConnectRecord to process + * @param attributes additional attributes to pass to the processing chain + * @return processing chain + */ + @Override + public Future> deliver(URI url, McpConnectRecord mcpConnectRecord, Map attributes, + ConnectRecord connectRecord) { + Failsafe.with(retryPolicy) + .getStageAsync(() -> sinkHandler.deliver(url, mcpConnectRecord, attributes, connectRecord).toCompletionStage()); + return null; + } + + + /** + * Cleans up and releases resources used by the Mcp/McpS handler. + */ + @Override + public void stop() { + sinkHandler.stop(); + } +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java index 5e85ac1078..7ae40504b5 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java @@ -1,19 +1,26 @@ -package org.apache.eventmesh.connector.mcp.source; +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ -import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.*; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.handler.codec.http.HttpObjectAggregator; -import io.netty.handler.codec.http.HttpServerCodec; -import io.netty.handler.stream.ChunkedWriteHandler; +package org.apache.eventmesh.connector.mcp.source; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; import org.apache.eventmesh.common.config.connector.Config; import org.apache.eventmesh.common.config.connector.mcp.McpSourceConfig; import org.apache.eventmesh.common.exception.EventMeshException; +import org.apache.eventmesh.connector.mcp.source.data.McpResponse; import org.apache.eventmesh.connector.mcp.source.protocol.Protocol; import org.apache.eventmesh.connector.mcp.source.protocol.ProtocolFactory; import org.apache.eventmesh.openconnect.api.ConnectorCreateService; @@ -22,39 +29,38 @@ import org.apache.eventmesh.openconnect.api.source.Source; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; -import java.util.*; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -/** - * MCP Source Connector (Netty Implementation) - * - * 职责: - * 1. 连接器生命周期管理(启动、停止) - * 2. Netty服务器的创建和管理 - * 3. 请求队列的管理 - * 4. 数据轮询和批处理 - * 5. 连接器配置管理 - * 6. 错误处理和监控 - */ +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.ext.web.Route; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.LoggerHandler; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + @Slf4j public class McpSourceConnector implements Source, ConnectorCreateService { private McpSourceConfig sourceConfig; - // Getter methods for testing and monitoring - @Getter + private BlockingQueue queue; + private int batchSize; - @Getter + + private Route route; + private Protocol protocol; - // Netty 服务器组件 - @Getter - private EventLoopGroup bossGroup; - @Getter - private EventLoopGroup workerGroup; - private Channel serverChannel; + private HttpServer server; @Getter private volatile boolean started = false; @@ -62,6 +68,7 @@ public class McpSourceConnector implements Source, ConnectorCreateService configClass() { return McpSourceConfig.class; @@ -85,144 +92,60 @@ public void init(ConnectorContext connectorContext) { doInit(); } - /** - * 初始化连接器 - */ private void doInit() { - // 初始化队列 + // init queue int maxQueueSize = this.sourceConfig.getConnectorConfig().getMaxStorageSize(); this.queue = new LinkedBlockingQueue<>(maxQueueSize); - // 初始化批处理大小 + // init batch size this.batchSize = this.sourceConfig.getConnectorConfig().getBatchSize(); - // 初始化协议处理器 + // init protocol String protocolName = this.sourceConfig.getConnectorConfig().getProtocol(); this.protocol = ProtocolFactory.getInstance(this.sourceConfig.connectorConfig, protocolName); - // 初始化协议处理器(传入队列引用) - this.protocol.initialize(this.sourceConfig.connectorConfig, this.queue); + final Vertx vertx = Vertx.vertx(); + final Router router = Router.router(vertx); + route = router.route() + .path(this.sourceConfig.connectorConfig.getPath()) + .handler(LoggerHandler.create()); - // 初始化 Netty 事件循环组 - this.bossGroup = new NioEventLoopGroup(1); - this.workerGroup = new NioEventLoopGroup(); + // set protocol handler + this.protocol.setHandler(route, queue); - log.info("McpSourceConnector initialized with protocol: {}, maxQueueSize: {}, batchSize: {}", - protocolName, maxQueueSize, batchSize); + // create server + this.server = vertx.createHttpServer(new HttpServerOptions() + .setPort(this.sourceConfig.connectorConfig.getPort()) + .setMaxFormAttributeSize(this.sourceConfig.connectorConfig.getMaxFormAttributeSize()) + .setIdleTimeout(this.sourceConfig.connectorConfig.getIdleTimeout()) + .setIdleTimeoutUnit(TimeUnit.MILLISECONDS)).requestHandler(router); } @Override public void start() { - try { - // 创建Netty服务器 - ServerBootstrap bootstrap = new ServerBootstrap(); - bootstrap.group(bossGroup, workerGroup) - .channel(NioServerSocketChannel.class) - .option(ChannelOption.SO_BACKLOG, 1024) - .option(ChannelOption.SO_REUSEADDR, true) - .childOption(ChannelOption.SO_KEEPALIVE, true) - .childOption(ChannelOption.TCP_NODELAY, true) - .childHandler(new ChannelInitializer() { - @Override - protected void initChannel(SocketChannel ch) { - ChannelPipeline pipeline = ch.pipeline(); - - // HTTP 编解码器 - pipeline.addLast("httpCodec", new HttpServerCodec()); - - // HTTP 对象聚合器 - pipeline.addLast("httpAggregator", new HttpObjectAggregator( - sourceConfig.getConnectorConfig().getMaxFormAttributeSize())); - - // 支持分块传输 - pipeline.addLast("chunkedWriter", new ChunkedWriteHandler()); - - // 协议处理器 - pipeline.addLast("protocolHandler", protocol.createHandler(sourceConfig)); - } - }); - - // 绑定端口并启动服务器 - int port = sourceConfig.getConnectorConfig().getPort(); - ChannelFuture future = bootstrap.bind(port).sync(); - this.serverChannel = future.channel(); - this.started = true; - - log.info("Netty McpSourceConnector started successfully on port: {}", port); - - // 启动后台任务 - startBackgroundTasks(); - - } catch (Exception e) { - log.error("Failed to start Netty McpSourceConnector", e); - throw new EventMeshException("Failed to start Netty server", e); - } - } - - /** - * 启动后台任务 - */ - private void startBackgroundTasks() { - // 启动队列监控任务 - workerGroup.scheduleAtFixedRate(() -> { - try { - monitorQueue(); - } catch (Exception e) { - log.error("Error in queue monitoring", e); + this.server.listen(res -> { + if (res.succeeded()) { + this.started = true; + log.info("McpSourceConnector started on port: {}", this.sourceConfig.getConnectorConfig().getPort()); + } else { + log.error("McpSourceConnector failed to start on port: {}", this.sourceConfig.getConnectorConfig().getPort()); + throw new EventMeshException("failed to start Vertx server", res.cause()); } - }, 10, 30, TimeUnit.SECONDS); - - // 启动健康检查任务 - workerGroup.scheduleAtFixedRate(() -> { - try { - performHealthCheck(); - } catch (Exception e) { - log.error("Error in health check", e); - } - }, 5, 60, TimeUnit.SECONDS); - } - - /** - * 监控队列状态 - */ - private void monitorQueue() { - int queueSize = queue.size(); - int maxQueueSize = sourceConfig.getConnectorConfig().getMaxStorageSize(); - - if (queueSize > maxQueueSize * 0.8) { - log.warn("Queue usage is high: {}/{} ({}%)", queueSize, maxQueueSize, - (queueSize * 100 / maxQueueSize)); - } - - if (queueSize > 0) { - log.debug("Queue status: {}/{} items", queueSize, maxQueueSize); - } - } - - /** - * 执行健康检查 - */ - private void performHealthCheck() { - boolean isHealthy = serverChannel != null && serverChannel.isActive() && started && !destroyed; - - if (!isHealthy) { - log.warn("Health check failed: serverChannel.isActive={}, started={}, destroyed={}", - serverChannel != null ? serverChannel.isActive() : "null", started, destroyed); - } else { - log.debug("Health check passed"); - } + }); } @Override public void commit(ConnectRecord record) { if (sourceConfig.getConnectorConfig().isDataConsistencyEnabled()) { - log.debug("Committing record: {}", record.getRecordId()); - - // 委托给协议处理器进行提交 - try { - protocol.commit(record); - } catch (Exception e) { - log.error("Failed to commit record: {}", record.getRecordId(), e); + log.debug("McpSourceConnector commit record: {}", record.getRecordId()); + RoutingContext routingContext = (RoutingContext) record.getExtensionObj("routingContext"); + if (routingContext != null) { + routingContext.response() + .putHeader("content-type", "application/json") + .setStatusCode(HttpResponseStatus.OK.code()) + .end(McpResponse.success().toJsonStr()); + } else { + log.error("Failed to commit the record, routingContext is null, recordId: {}", record.getRecordId()); } } } @@ -234,47 +157,32 @@ public String name() { @Override public void onException(ConnectRecord record) { - log.error("Exception occurred for record: {}", record.getRecordId()); - - // 委托给协议处理器处理异常 - try { - protocol.onException(record); - } catch (Exception e) { - log.error("Failed to handle exception for record: {}", record.getRecordId(), e); + if (this.route != null) { + this.route.failureHandler(ctx -> { + log.error("Failed to handle the request, recordId {}. ", record.getRecordId(), ctx.failure()); + // Return Bad Response + ctx.response() + .setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()) + .end("{\"status\":\"failed\",\"recordId\":\"" + record.getRecordId() + "\"}"); + }); } } @Override public void stop() { - log.info("Stopping McpSourceConnector..."); - - try { - // 停止协议处理器 - if (protocol != null) { - protocol.shutdown(); - } - - // 关闭服务器通道 - if (serverChannel != null) { - serverChannel.close().sync(); - } - - // 优雅关闭事件循环组 - if (bossGroup != null) { - bossGroup.shutdownGracefully(2, 10, TimeUnit.SECONDS).sync(); - } - if (workerGroup != null) { - workerGroup.shutdownGracefully(2, 10, TimeUnit.SECONDS).sync(); - } - - this.destroyed = true; - this.started = false; - - log.info("McpSourceConnector stopped successfully"); - - } catch (Exception e) { - log.error("Failed to stop McpSourceConnector gracefully", e); - throw new EventMeshException("Failed to stop Netty server", e); + if (this.server != null) { + this.server.close(res -> { + if (res.succeeded()) { + this.destroyed = true; + log.info("McpSourceConnector stopped on port: {}", this.sourceConfig.getConnectorConfig().getPort()); + } else { + log.error("McpSourceConnector failed to stop on port: {}", this.sourceConfig.getConnectorConfig().getPort()); + throw new EventMeshException("failed to stop Vertx server", res.cause()); + } + } + ); + } else { + log.warn("McpSourceConnector server is null, ignore."); } } @@ -284,73 +192,28 @@ public List poll() { long maxPollWaitTime = 5000; long remainingTime = maxPollWaitTime; + // poll from queue List connectRecords = new ArrayList<>(batchSize); - - try { - for (int i = 0; i < batchSize; i++) { + for (int i = 0; i < batchSize; i++) { + try { Object obj = queue.poll(remainingTime, TimeUnit.MILLISECONDS); if (obj == null) { break; } - - // 委托给协议处理器转换记录 + // convert to ConnectRecord ConnectRecord connectRecord = protocol.convertToConnectRecord(obj); - if (connectRecord != null) { - connectRecords.add(connectRecord); - } + connectRecords.add(connectRecord); - // 计算剩余时间 + // calculate elapsed time and update remaining time for next poll long elapsedTime = System.currentTimeMillis() - startTime; - remainingTime = Math.max(0, maxPollWaitTime - elapsedTime); - } - - if (!connectRecords.isEmpty()) { - log.debug("Polled {} records from queue", connectRecords.size()); + remainingTime = maxPollWaitTime > elapsedTime ? maxPollWaitTime - elapsedTime : 0; + } catch (Exception e) { + log.error("Failed to poll from queue.", e); + throw new RuntimeException(e); } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.warn("Polling interrupted", e); - } catch (Exception e) { - log.error("Error during polling", e); } - return connectRecords; } - /** - * 获取队列统计信息 - */ - public Map getQueueStats() { - Map stats = new HashMap<>(); - stats.put("queueSize", queue.size()); - stats.put("maxQueueSize", sourceConfig.getConnectorConfig().getMaxStorageSize()); - stats.put("batchSize", batchSize); - stats.put("queueUtilization", (double) queue.size() / sourceConfig.getConnectorConfig().getMaxStorageSize()); - return stats; - } - - /** - * 获取服务器状态 - */ - public Map getServerStatus() { - Map status = new HashMap<>(); - status.put("started", started); - status.put("destroyed", destroyed); - status.put("serverActive", serverChannel != null && serverChannel.isActive()); - status.put("port", sourceConfig.getConnectorConfig().getPort()); - status.put("connectorName", name()); - return status; - } - - /** - * 强制清空队列 - */ - public int clearQueue() { - int clearedCount = queue.size(); - queue.clear(); - log.info("Cleared {} items from queue", clearedCount); - return clearedCount; - } - -} \ No newline at end of file +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/MCPStreamingResponse.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/MCPStreamingResponse.java deleted file mode 100644 index 478d93e517..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/MCPStreamingResponse.java +++ /dev/null @@ -1,210 +0,0 @@ -package org.apache.eventmesh.connector.mcp.source.data; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import java.io.Serializable; - -/** - * 流式结果类 - * 用于管理流式传输的数据块 - */ -@Slf4j -public class MCPStreamingResponse implements Serializable { - private final String[] chunks; - @Getter - private int currentIndex = 0; - @Getter - private final String originalContent; - - /** - * 构造函数 - * - * @param content 原始内容,将被分割成块 - */ - public MCPStreamingResponse(String content) { - this.originalContent = content; - this.chunks = splitContent(content); - log.debug("Created StreamingResult with {} chunks", chunks.length); - } - - /** - * 构造函数,支持自定义分割 - * - * @param content 原始内容 - * @param delimiter 分割符 - */ - public MCPStreamingResponse(String content, String delimiter) { - this.originalContent = content; - this.chunks = content.split(delimiter); - log.debug("Created StreamingResult with {} chunks using delimiter: {}", chunks.length, delimiter); - } - - /** - * 获取下一个数据块 - * - * @return 下一个数据块,如果没有更多数据则返回null - */ - public String getNextChunk() { - if (currentIndex < chunks.length) { - String chunk = chunks[currentIndex]; - currentIndex++; - - log.debug("Returning chunk {}/{}: {}", currentIndex, chunks.length, - chunk.length() > 50 ? chunk.substring(0, 50) + "..." : chunk); - - return chunk + " "; // 添加空格以保持词与词之间的间距 - } - - log.debug("No more chunks available"); - return null; - } - - /** - * 检查是否还有更多数据块 - * - * @return 如果还有数据块返回true,否则返回false - */ - public boolean hasMore() { - return currentIndex < chunks.length; - } - - /** - * 检查是否已完成 - * - * @return 如果所有数据块都已发送返回true,否则返回false - */ - public boolean isComplete() { - return currentIndex >= chunks.length; - } - - /** - * 获取总块数 - * - * @return 总块数 - */ - public int getTotalChunks() { - return chunks.length; - } - - /** - * 获取进度百分比 - * - * @return 进度百分比(0-100) - */ - public int getProgress() { - if (chunks.length == 0) { - return 100; - } - return (currentIndex * 100) / chunks.length; - } - - /** - * 重置到开始位置 - */ - public void reset() { - this.currentIndex = 0; - log.debug("StreamingResult reset to beginning"); - } - - /** - * 跳到指定位置 - * - * @param index 目标索引 - */ - public void seekTo(int index) { - if (index >= 0 && index <= chunks.length) { - this.currentIndex = index; - log.debug("StreamingResult seeked to index: {}", index); - } else { - log.warn("Invalid seek index: {}, valid range: 0-{}", index, chunks.length); - } - } - - /** - * 获取剩余块数 - * - * @return 剩余块数 - */ - public int getRemainingChunks() { - return Math.max(0, chunks.length - currentIndex); - } - - /** - * 预览下一个块(不移动指针) - * - * @return 下一个块的内容,如果没有则返回null - */ - public String peekNext() { - if (currentIndex < chunks.length) { - return chunks[currentIndex] + " "; - } - return null; - } - - /** - * 获取所有剩余的块 - * - * @return 剩余块的数组 - */ - public String[] getRemainingChunksArray() { - if (currentIndex >= chunks.length) { - return new String[0]; - } - - String[] remaining = new String[chunks.length - currentIndex]; - System.arraycopy(chunks, currentIndex, remaining, 0, remaining.length); - return remaining; - } - - /** - * 分割内容的私有方法 - * - * @param content 要分割的内容 - * @return 分割后的数组 - */ - private String[] splitContent(String content) { - if (content == null || content.trim().isEmpty()) { - return new String[]{"Empty content"}; - } - - // 优先按句号分割,保持句子完整性 - String[] sentences = content.split("。"); - if (sentences.length > 1) { - // 为非最后一个句子添加句号 - for (int i = 0; i < sentences.length - 1; i++) { - if (!sentences[i].trim().isEmpty()) { - sentences[i] = sentences[i].trim() + "。"; - } - } - // 过滤空字符串 - return java.util.Arrays.stream(sentences) - .filter(s -> !s.trim().isEmpty()) - .toArray(String[]::new); - } - - // 如果没有句号,按逗号分割 - String[] phrases = content.split(",|,"); - if (phrases.length > 1) { - return java.util.Arrays.stream(phrases) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .toArray(String[]::new); - } - - // 如果没有逗号,按空格分割词语 - String[] words = content.trim().split("\\s+"); - if (words.length > 5) { // 如果词语太多,按词分割 - return words; - } - - // 如果词语较少,保持原内容 - return new String[]{content.trim()}; - } - - @Override - public String toString() { - return String.format("StreamingResult{totalChunks=%d, currentIndex=%d, progress=%d%%, complete=%s}", - chunks.length, currentIndex, getProgress(), isComplete()); - } -} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java index 107f369768..8743cf7ac9 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java @@ -1,13 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.source.data; +import java.io.Serializable; +import java.util.Map; + import io.vertx.ext.web.RoutingContext; + import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.apache.eventmesh.connector.mcp.session.MCPSession; - -import java.io.Serializable; -import java.util.Map; /** * Mcp Protocol Request. @@ -16,44 +33,20 @@ @NoArgsConstructor @AllArgsConstructor public class McpRequest implements Serializable { + private static final long serialVersionUID = -483500600756490500L; - private String protocol; - private String absoluteURI; - private Map headerMap; - private boolean isStreaming; - private Map payloadMap; - private RoutingContext ctx; + private String protocolName; + private String sessionId; - private String protocolVersion; - private MCPSession session; - - // 构造器,为了兼容原有代码 - public McpRequest(String protocol, String absoluteURI, Map headerMap, - boolean isStreaming, Map payloadMap, RoutingContext ctx) { - this.protocol = protocol; - this.absoluteURI = absoluteURI; - this.headerMap = headerMap; - this.isStreaming = isStreaming; - this.payloadMap = payloadMap; - this.ctx = ctx; - } - - // 为了兼容原有代码,保留这些方法 - public String getProtocolName() { - return protocol; - } - - public Object getInputs() { - return payloadMap != null ? payloadMap.get("inputs") : null; - } - - public Map getMetadata() { - return payloadMap != null ? (Map) payloadMap.get("metadata") : null; - } - - public RoutingContext getRoutingContext() { - return ctx; - } -} + // 元信息,用于携带请求的附加信息,如用户 ID、时间戳、调用方信息、模型版本、语言设置等。服务端可以据此做日志、权限判断、模型路由等操作 + private Map metadata; + + // 用户输入内容,通常是一个 List,每个元素是一个 Map,表示一轮对话,例如: + //[{"role":"user","content":"你好"}, {"role":"assistant","content":"你好呀"}]。 + private Object inputs; + + private RoutingContext routingContext; + +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java index ff8717e600..32e2910f7a 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.source.data; import lombok.AllArgsConstructor; @@ -11,7 +28,7 @@ import java.util.Map; /** - * Webhook response. + * Mcp response. */ @Data @NoArgsConstructor @@ -19,26 +36,36 @@ public class McpResponse implements Serializable { private static final long serialVersionUID = 8616938575207104455L; - private Object outputs; - - private String sessionId; - - private Map metadata; - - private String finishReason; + private String msg; private LocalDateTime handleTime; + /** + * Convert to json string. + * + * @return json string + */ public String toJsonStr() { return JSON.toJSONString(this, Feature.WriteMapNullValue); } - public static McpResponse success(Object outputs, String sessionId) { - return new McpResponse(outputs, sessionId, null, "done", LocalDateTime.now()); + /** + * Create a success response. + * + * @return response + */ + public static McpResponse success() { + return base("success"); } - public static McpResponse base(Object outputs, String sessionId, String finishReason) { - return new McpResponse(outputs, sessionId, null, finishReason, LocalDateTime.now()); + /** + * Create a base response. + * + * @param msg message + * @return response + */ + public static McpResponse base(String msg) { + return new McpResponse(msg, LocalDateTime.now()); } } diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpStreamingRequest.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpStreamingRequest.java deleted file mode 100644 index 85cb524124..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpStreamingRequest.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.apache.eventmesh.connector.mcp.source.data; - -import io.vertx.ext.web.RoutingContext; -import lombok.Data; -import org.apache.eventmesh.connector.mcp.session.MCPSession; - -import java.util.Map; /** - * Mcp Streaming Request - */ -@Data -public class McpStreamingRequest extends McpRequest { - - public McpStreamingRequest(String protocol, String absoluteURI, Map headerMap, - boolean isStreaming, Map payloadMap, - RoutingContext ctx, MCPSession session) { - super(protocol, absoluteURI, headerMap, isStreaming, payloadMap, ctx); - setSession(session); - } - - public McpStreamingRequest(String protocol, String absoluteURI, Map headerMap, - boolean isStreaming, Map payloadMap, - RoutingContext ctx, String sessionId, String protocolVersion, MCPSession session) { - super(protocol, absoluteURI, headerMap, isStreaming, payloadMap, ctx, sessionId, protocolVersion, session); - } -} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/handler/NettyMcpRequestHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/handler/NettyMcpRequestHandler.java deleted file mode 100644 index 53c2b95ce0..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/handler/NettyMcpRequestHandler.java +++ /dev/null @@ -1,614 +0,0 @@ -package org.apache.eventmesh.connector.mcp.source.handler; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.handler.codec.http.*; - import io.netty.util.CharsetUtil; - -import lombok.extern.slf4j.Slf4j; -import org.apache.eventmesh.common.config.connector.mcp.McpSourceConfig; -import org.apache.eventmesh.connector.mcp.source.data.McpRequest; -import org.apache.eventmesh.connector.mcp.source.data.McpStreamingRequest; -import org.apache.eventmesh.connector.mcp.util.AIToolManager; -import org.apache.eventmesh.connector.mcp.source.data.MCPStreamingResponse; -import org.apache.eventmesh.connector.mcp.session.MCPSession; -import org.apache.eventmesh.connector.mcp.session.SessionManager; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.core.type.TypeReference; -import org.apache.eventmesh.connector.mcp.util.McpResponseValidator; - -import java.util.*; - import java.util.concurrent.BlockingQueue; -import java.util.concurrent.TimeUnit; - -/** - * Netty MCP请求处理器 - */ -@Slf4j -public class NettyMcpRequestHandler extends SimpleChannelInboundHandler { - - private static final String PROTOCOL_NAME = "Mcp"; - private final McpSourceConfig sourceConfig; - private final BlockingQueue queue; - private final SessionManager sessionManager; - private final ObjectMapper objectMapper; - private final AIToolManager toolManager; - - public NettyMcpRequestHandler(McpSourceConfig sourceConfig, BlockingQueue queue, - SessionManager sessionManager, ObjectMapper objectMapper, - AIToolManager toolManager) { - this.sourceConfig = sourceConfig; - this.queue = queue; - this.sessionManager = sessionManager; - this.objectMapper = objectMapper; - this.toolManager = toolManager; - } - - @Override - protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { - // 处理CORS预检请求 - if (request.method() == HttpMethod.OPTIONS) { - sendCorsResponse(ctx); - return; - } - - // 处理健康检查GET请求 - if (request.method() == HttpMethod.GET) { - // 检查是否是SSE请求 - String accept = request.headers().get("Accept"); - if (accept != null && accept.contains("text/event-stream")) { - handleSSERequest(ctx, request); - return; - } else { - sendHealthCheckResponse(ctx); - return; - } - } - - // 检查路径是否匹配 - String targetPath = sourceConfig.getConnectorConfig().getPath(); - if (!targetPath.equals(request.uri())) { - sendNotFound(ctx); - return; - } - - // 只处理POST请求用于MCP调用 - if (request.method() != HttpMethod.POST) { - sendMethodNotAllowed(ctx); - return; - } - - try { - // 解析请求体 - String jsonStr = request.content().toString(CharsetUtil.UTF_8); - Map payloadMap = objectMapper.readValue(jsonStr, new TypeReference>() {}); - - // 从请求头获取MCP相关信息 - String mcpProtocolVersion = request.headers().get("Mcp-Protocol-Version"); - String mcpSessionId = request.headers().get("Mcp-Session-Id"); - - log.info("Received MCP request - Protocol: {}, Session: {}, URI: {}", - mcpProtocolVersion, mcpSessionId, request.uri()); - - // 检查是否是流式请求 - Boolean isStreaming = (Boolean) payloadMap.get("stream"); - if (Boolean.TRUE.equals(isStreaming)) { - handleStreamingRequest(ctx, request, payloadMap, mcpSessionId, mcpProtocolVersion); - } else { - handleNormalRequest(ctx, request, payloadMap, mcpSessionId, mcpProtocolVersion); - } - - } catch (Exception e) { - log.error("Error processing MCP request", e); - sendError(ctx, "Invalid request format: " + e.getMessage()); - } - } - - /** - * 处理普通MCP请求 - */ - private void handleNormalRequest(ChannelHandlerContext ctx, FullHttpRequest request, - Map payloadMap, String sessionId, String protocolVersion) { - - // 获取或创建会话 - MCPSession session = getOrCreateSession(sessionId); - session.updateLastActivity(); - - // 解析MCP请求结构 - List> inputs = (List>) payloadMap.get("inputs"); - Map context = (Map) payloadMap.get("context"); - Map metadata = (Map) payloadMap.get("metadata"); - - // 创建MCP请求对象 - Map headerMap = new HashMap<>(); - for (Map.Entry entry : request.headers().entries()) { - headerMap.put(entry.getKey(), entry.getValue()); - } - - McpRequest mcpRequest = new McpRequest( - PROTOCOL_NAME, - request.uri(), - headerMap, - false, - payloadMap, - null, // Netty没有RoutingContext - sessionId, - protocolVersion, - session - ); - - // 添加到队列 - if (!queue.offer(mcpRequest)) { - sendError(ctx, "Queue full, failed to process request"); - return; - } - - // 如果启用了数据一致性,处理请求并返回响应 - if (sourceConfig.getConnectorConfig().isDataConsistencyEnabled()) { - processNormalMcpRequest(ctx, inputs, context, metadata, session, sessionId, protocolVersion); - } - } - - /** - * 处理流式MCP请求 - */ - private void handleStreamingRequest(ChannelHandlerContext ctx, FullHttpRequest request, - Map payloadMap, String sessionId, String protocolVersion) { - - // 获取或创建会话 - MCPSession session = getOrCreateSession(sessionId); - session.updateLastActivity(); - - // 解析MCP请求结构 - List> inputs = (List>) payloadMap.get("inputs"); - Map context = (Map) payloadMap.get("context"); - Map metadata = (Map) payloadMap.get("metadata"); - - // 设置流式响应头 - HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); - response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); - response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); - response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS"); - response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, Authorization, Mcp-Protocol-Version, Mcp-Session-Id"); - - if (protocolVersion != null) { - response.headers().set("Mcp-Protocol-Version", protocolVersion); - } - if (sessionId != null) { - response.headers().set("Mcp-Session-Id", sessionId); - } - - // 发送响应头 - ctx.write(response); - - // 创建流式请求对象 - Map headerMap = new HashMap<>(); - for (Map.Entry entry : request.headers().entries()) { - headerMap.put(entry.getKey(), entry.getValue()); - } - - McpStreamingRequest streamingRequest = new McpStreamingRequest( - PROTOCOL_NAME, - request.uri(), - headerMap, - true, - payloadMap, - null, // Netty没有RoutingContext - sessionId, - protocolVersion, - session - ); - - // 添加到队列 - if (!queue.offer(streamingRequest)) { - sendStreamingError(ctx, sessionId, "Queue full", protocolVersion); - return; - } - - // 异步处理流式响应 - processStreamingMcpRequest(ctx, inputs, context, metadata, session, sessionId, protocolVersion); - } - - /** - * 处理普通MCP请求 - */ - private void processNormalMcpRequest(ChannelHandlerContext ctx, List> inputs, - Map context, Map metadata, - MCPSession session, String sessionId, String protocolVersion) { - - // 提取用户输入 - String userContent = extractUserContent(inputs); - - // 设置会话上下文 - if (context != null) { - session.setContext("history", context.get("history")); - session.setContext("context", context); - } - - // 异步处理请求 - toolManager.processRequest(userContent, session, metadata) - .thenAccept(result -> { - try { - // 构造MCP响应 - Map responseMap = new HashMap<>(); - responseMap.put("session_id", sessionId); - - Map output = new HashMap<>(); - output.put("role", "assistant"); - output.put("content", result); - responseMap.put("outputs", Arrays.asList(output)); - - Map metadataMap = new HashMap<>(); - metadataMap.put("timestamp", System.currentTimeMillis()); - metadataMap.put("version", protocolVersion != null ? protocolVersion : "2025-03-26"); - metadataMap.put("streaming", false); - responseMap.put("metadata", metadataMap); - - String responseJson = objectMapper.writeValueAsString(responseMap); - - // 发送响应 - ByteBuf content = Unpooled.copiedBuffer(responseJson, CharsetUtil.UTF_8); - FullHttpResponse response = new DefaultFullHttpResponse( - HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content); - - response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); - response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); - response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - - if (protocolVersion != null) { - response.headers().set("Mcp-Protocol-Version", protocolVersion); - } - if (sessionId != null) { - response.headers().set("Mcp-Session-Id", sessionId); - } - - ctx.writeAndFlush(response); - - } catch (Exception e) { - log.error("Error processing response", e); - sendError(ctx, "Error processing response: " + e.getMessage()); - } - }) - .exceptionally(throwable -> { - log.error("Error processing request", throwable); - sendError(ctx, "Error processing request: " + throwable.getMessage()); - return null; - }); - } - - /** - * 处理流式MCP请求 - */ - private void processStreamingMcpRequest(ChannelHandlerContext ctx, List> inputs, - Map context, Map metadata, - MCPSession session, String sessionId, String protocolVersion) { - - // 提取用户输入 - String userContent = extractUserContent(inputs); - - // 设置会话上下文 - if (context != null) { - session.setContext("history", context.get("history")); - session.setContext("context", context); - } - - // 异步处理流式请求 - toolManager.processStreamingRequest(userContent, session, metadata) - .thenAccept(streamingResult -> { - startMcpStreaming(ctx, sessionId, streamingResult, protocolVersion); - }) - .exceptionally(throwable -> { - log.error("Error processing streaming request", throwable); - sendStreamingError(ctx, sessionId, throwable.getMessage(), protocolVersion); - return null; - }); - } - - /** - * 开始MCP流式传输 - */ - private void startMcpStreaming(ChannelHandlerContext ctx, String sessionId, - MCPStreamingResponse result, String protocolVersion) { - - // 使用Netty的事件循环定时器实现流式传输 - ctx.executor().scheduleAtFixedRate(() -> { - if (!ctx.channel().isActive()) { - return; - } - - try { - // 获取下一个数据块 - String chunk = result.getNextChunk(); - if (chunk != null) { - // 使用验证器创建标准的MCP响应 - String responseJson = McpResponseValidator.createStreamingResponse( - sessionId, - chunk, - result.getCurrentIndex() - 1, - result.isComplete(), - protocolVersion - ); - - log.debug("Streaming chunk {}/{}: {}", - result.getCurrentIndex() - 1, - result.getTotalChunks(), - chunk.substring(0, Math.min(50, chunk.length()))); - - ByteBuf chunkBuf = Unpooled.copiedBuffer(responseJson + "\n", CharsetUtil.UTF_8); - ctx.writeAndFlush(new DefaultHttpContent(chunkBuf)); - - // 如果完成,结束流式传输 - if (result.isComplete()) { - ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); - log.info("Streaming completed for session: {} with {} chunks", - sessionId, result.getTotalChunks()); - return; - } - } - } catch (Exception e) { - log.error("Error in streaming for session: {}", sessionId, e); - sendStreamingError(ctx, sessionId, e.getMessage(), protocolVersion); - } - }, 0, 300, TimeUnit.MILLISECONDS); // 稍微降低频率到300ms - } - - - /** - * 提取用户输入内容 - */ - private String extractUserContent(List> inputs) { - if (inputs == null || inputs.isEmpty()) { - return ""; - } - - // 查找用户角色的消息 - for (Map input : inputs) { - if ("user".equals(input.get("role"))) { - return (String) input.get("content"); - } - } - - // 如果没有找到用户角色,返回第一个输入的内容 - return (String) inputs.get(0).get("content"); - } - - /** - * 发送流式错误 - */ - private void sendStreamingError(ChannelHandlerContext ctx, String sessionId, - String errorMessage, String protocolVersion) { - try { - // 使用验证器创建标准错误响应 - String errorJson = McpResponseValidator.createErrorResponse( - sessionId, errorMessage, "STREAMING_ERROR", protocolVersion); - - ByteBuf errorBuf = Unpooled.copiedBuffer(errorJson + "\n", CharsetUtil.UTF_8); - ctx.writeAndFlush(new DefaultHttpContent(errorBuf)); - ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); - - log.error("Sent streaming error for session {}: {}", sessionId, errorMessage); - - } catch (Exception e) { - log.error("Error sending streaming error for session: {}", sessionId, e); - // 最后的备用方案 - String fallbackError = "{\"error\":{\"message\":\"Internal error\",\"code\":\"INTERNAL_ERROR\"},\"session_id\":\"" + sessionId + "\"}\n"; - ByteBuf fallbackBuf = Unpooled.copiedBuffer(fallbackError, CharsetUtil.UTF_8); - ctx.writeAndFlush(new DefaultHttpContent(fallbackBuf)); - ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); - } - } - - /** - * 获取或创建会话 - */ - private MCPSession getOrCreateSession(String sessionId) { - if (sessionId == null || sessionId.isEmpty()) { - // 如果没有提供会话ID,创建新会话 - MCPSession newSession = sessionManager.createSession(); - log.info("Created new session: {}", newSession.getSessionId()); - return newSession; - } - - // 尝试获取现有会话 - MCPSession session = sessionManager.getSession(sessionId); - if (session == null) { - // 如果会话不存在,使用提供的会话ID创建新会话 - session = sessionManager.createSessionWithId(sessionId); - log.info("Created session with provided ID: {}", sessionId); - } else { - log.debug("Using existing session: {}", sessionId); - } - - return session; - } - - /** - * 发送错误响应 - */ - private void sendError(ChannelHandlerContext ctx, String message) { - try { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", Map.of( - "message", message, - "code", "INVALID_REQUEST" - )); - errorResponse.put("metadata", Map.of( - "timestamp", System.currentTimeMillis(), - "version", "2025-03-26" - )); - - String json = objectMapper.writeValueAsString(errorResponse); - ByteBuf content = Unpooled.copiedBuffer(json, CharsetUtil.UTF_8); - - FullHttpResponse response = new DefaultFullHttpResponse( - HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST, content); - - response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); - response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); - response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - - ctx.writeAndFlush(response); - } catch (Exception e) { - log.error("Error sending error response", e); - } - } - - /** - * 发送404响应 - */ - private void sendNotFound(ChannelHandlerContext ctx) { - FullHttpResponse response = new DefaultFullHttpResponse( - HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); - - response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - ctx.writeAndFlush(response); - } - - /** - * 处理SSE请求 - */ - private void handleSSERequest(ChannelHandlerContext ctx, FullHttpRequest request) { - log.info("Handling SSE request from: {}", ctx.channel().remoteAddress()); - - // 设置SSE响应头 - HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); - response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/event-stream"); - response.headers().set(HttpHeaderNames.CACHE_CONTROL, "no-cache"); - response.headers().set(HttpHeaderNames.CONNECTION, "keep-alive"); - response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Cache-Control"); - - // 发送响应头 - ctx.write(response); - - // 发送初始连接事件 - sendSSEEvent(ctx, "connected", Map.of( - "status", "connected", - "server", "EventMesh MCP Server", - "version", "2025-03-26", - "timestamp", System.currentTimeMillis() - )); - - // 设置定时心跳 - ctx.executor().scheduleAtFixedRate(() -> { - if (ctx.channel().isActive()) { - sendSSEEvent(ctx, "heartbeat", Map.of( - "timestamp", System.currentTimeMillis(), - "status", "alive" - )); - } - }, 30, 30, java.util.concurrent.TimeUnit.SECONDS); - - // 发送服务器能力信息 - sendSSEEvent(ctx, "capabilities", Map.of( - "tools", Arrays.asList("text-generator", "code-analyzer", "data-processor"), - "streaming", true, - "protocol_version", "2025-03-26" - )); - } - - /** - * 发送SSE事件 - */ - private void sendSSEEvent(ChannelHandlerContext ctx, String eventType, Object data) { - try { - String eventData = objectMapper.writeValueAsString(data); - String sseEvent = String.format("event: %s\ndata: %s\n\n", eventType, eventData); - - ByteBuf eventBuf = Unpooled.copiedBuffer(sseEvent, CharsetUtil.UTF_8); - ctx.writeAndFlush(new DefaultHttpContent(eventBuf)); - - log.debug("Sent SSE event: {} with data: {}", eventType, eventData); - } catch (Exception e) { - log.error("Error sending SSE event", e); - } - } - - /** - * 发送CORS预检响应 - */ - private void sendCorsResponse(ChannelHandlerContext ctx) { - FullHttpResponse response = new DefaultFullHttpResponse( - HttpVersion.HTTP_1_1, HttpResponseStatus.OK); - - response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS"); - response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, Authorization, Mcp-Protocol-Version, Mcp-Session-Id"); - response.headers().set(HttpHeaderNames.ACCESS_CONTROL_MAX_AGE, "86400"); - - ctx.writeAndFlush(response); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - log.error("Exception caught in channel", cause); - ctx.close(); - } - - /** - * 发送健康检查响应 - */ - private void sendHealthCheckResponse(ChannelHandlerContext ctx) { - try { - Map healthResponse = new HashMap<>(); - healthResponse.put("status", "healthy"); - healthResponse.put("service", "EventMesh MCP Server"); - healthResponse.put("version", "2025-03-26"); - healthResponse.put("timestamp", System.currentTimeMillis()); - healthResponse.put("endpoints", Map.of( - "mcp", "POST /test", - "health", "GET /test", - "sse", "GET /test (with Accept: text/event-stream)" - )); - - String json = objectMapper.writeValueAsString(healthResponse); - ByteBuf content = Unpooled.copiedBuffer(json, CharsetUtil.UTF_8); - - FullHttpResponse response = new DefaultFullHttpResponse( - HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content); - - response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); - response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); - response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, OPTIONS"); - response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, Mcp-Protocol-Version, Mcp-Session-Id, Accept"); - - ctx.writeAndFlush(response); - log.debug("Sent health check response"); - } catch (Exception e) { - log.error("Error sending health check response", e); - sendError(ctx, "Health check failed"); - } - } - - /** - * 发送405响应 - */ - private void sendMethodNotAllowed(ChannelHandlerContext ctx) { - try { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "Method not allowed"); - errorResponse.put("message", "This endpoint only accepts POST requests for MCP calls and GET requests for health checks"); - errorResponse.put("allowed_methods", Arrays.asList("GET", "POST", "OPTIONS")); - - String json = objectMapper.writeValueAsString(errorResponse); - ByteBuf content = Unpooled.copiedBuffer(json, CharsetUtil.UTF_8); - - FullHttpResponse response = new DefaultFullHttpResponse( - HttpVersion.HTTP_1_1, HttpResponseStatus.METHOD_NOT_ALLOWED, content); - - response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); - response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); - response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - response.headers().set(HttpHeaderNames.ALLOW, "GET, POST, OPTIONS"); - - ctx.writeAndFlush(response); - } catch (Exception e) { - log.error("Error sending method not allowed response", e); - } - } -} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/McpConstants.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/McpConstants.java index 1d7ece2979..dee8afd82c 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/McpConstants.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/McpConstants.java @@ -1,5 +1,5 @@ package org.apache.eventmesh.connector.mcp.source.protocol; -// header key、content-type 常量等 + public class McpConstants { } diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java index 61a8ebd39c..d92669b769 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java @@ -1,144 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.source.protocol; -import io.netty.channel.ChannelHandler; import io.vertx.ext.web.Route; -import org.apache.eventmesh.common.config.connector.mcp.McpSourceConfig; import org.apache.eventmesh.common.config.connector.mcp.SourceConnectorConfig; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; import java.util.concurrent.BlockingQueue; /** - * Protocol interface for MCP connectors - * - * 定义了协议处理器的基本接口,支持不同的传输层实现(Netty/Vert.x) + * Protocol Interface. + * All protocols should implement this interface. */ public interface Protocol { /** - * 初始化协议处理器 + * Initialize the protocol. * - * @param sourceConnectorConfig 源连接器配置 + * @param sourceConnectorConfig source connector config */ void initialize(SourceConnectorConfig sourceConnectorConfig); - /** - * 初始化协议处理器(带队列) - * - * @param sourceConnectorConfig 源连接器配置 - * @param queue 请求队列 - */ - default void initialize(SourceConnectorConfig sourceConnectorConfig, BlockingQueue queue) { - initialize(sourceConnectorConfig); - } /** - * 创建 Netty 请求处理器 + * Handle the protocol message. * - * @param sourceConfig MCP源配置 - * @return Netty ChannelHandler + * @param route route + * @param queue queue info */ - default ChannelHandler createHandler(McpSourceConfig sourceConfig) { - throw new UnsupportedOperationException("Netty handler creation not supported by this protocol implementation"); - } + void setHandler(Route route, BlockingQueue queue); - /** - * 设置 Vert.x 路由处理器 - * - * @deprecated 推荐使用 createHandler(McpSourceConfig) 来创建 Netty 处理器 - * @param route Vert.x 路由 - * @param queue 请求队列 - */ - @Deprecated - default void setHandler(Route route, BlockingQueue queue) { - throw new UnsupportedOperationException("Vert.x route handling not supported by this protocol implementation"); - } /** - * 将消息转换为 ConnectRecord + * Convert the message to ConnectRecord. * - * @param message 原始消息 - * @return 转换后的 ConnectRecord + * @param message message + * @return ConnectRecord */ ConnectRecord convertToConnectRecord(Object message); - - /** - * 提交记录处理结果 - * - * @param record 要提交的记录 - */ - default void commit(ConnectRecord record) { - // 默认实现为空,子类可以重写 - } - - /** - * 处理记录异常 - * - * @param record 发生异常的记录 - */ - default void onException(ConnectRecord record) { - // 默认实现为空,子类可以重写 - } - - /** - * 关闭协议处理器 - */ - default void shutdown() { - // 默认实现为空,子类可以重写 - } - - /** - * 获取协议名称 - * - * @return 协议名称 - */ - default String getProtocolName() { - return this.getClass().getSimpleName(); - } - - /** - * 获取协议版本 - * - * @return 协议版本 - */ - default String getProtocolVersion() { - return "1.0"; - } - - /** - * 检查协议是否支持流式传输 - * - * @return 是否支持流式传输 - */ - default boolean supportsStreaming() { - return false; - } - - /** - * 检查协议是否支持压缩 - * - * @return 是否支持压缩 - */ - default boolean supportsCompression() { - return false; - } - - /** - * 获取协议配置信息 - * - * @return 配置信息映射 - */ - default java.util.Map getProtocolConfig() { - return java.util.Collections.emptyMap(); - } - - /** - * 验证协议配置 - * - * @param config 要验证的配置 - * @return 验证结果 - */ - default boolean validateConfig(SourceConnectorConfig config) { - return true; - } } \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/ProtocolFactory.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/ProtocolFactory.java index 883992a85d..1c30d4e43e 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/ProtocolFactory.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/ProtocolFactory.java @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.source.protocol; import org.apache.eventmesh.common.config.connector.mcp.SourceConnectorConfig; diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java index 2c342cd8b9..74ae98eca1 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java @@ -1,460 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.source.protocol.impl; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.netty.channel.ChannelHandler; -import io.vertx.ext.web.Route; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.apache.eventmesh.common.config.connector.mcp.McpSourceConfig; +import org.apache.eventmesh.common.Constants; import org.apache.eventmesh.common.config.connector.mcp.SourceConnectorConfig; +import org.apache.eventmesh.common.utils.JsonUtils; import org.apache.eventmesh.connector.mcp.source.data.McpRequest; -import org.apache.eventmesh.connector.mcp.source.handler.NettyMcpRequestHandler; +import org.apache.eventmesh.connector.mcp.source.data.McpResponse; import org.apache.eventmesh.connector.mcp.source.protocol.Protocol; -import org.apache.eventmesh.connector.mcp.util.AIToolManager; -import org.apache.eventmesh.connector.mcp.session.MCPSession; -import org.apache.eventmesh.connector.mcp.session.SessionManager; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; -import org.apache.eventmesh.connector.mcp.util.CloudEventsExtensionUtil; - -import java.util.*; +import java.util.Base64; +import java.util.Map; import java.util.concurrent.BlockingQueue; +import java.util.stream.Collectors; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Route; +import io.vertx.ext.web.handler.BodyHandler; + +import lombok.extern.slf4j.Slf4j; /** - * MCP Standard Protocol Implementation (Netty-based) - * - * 职责: - * 1. MCP协议的具体实现和规范遵循 - * 2. 请求/响应格式的解析和构造 - * 3. 数据转换和验证 - * 4. 协议级别的错误处理 + * Common Protocol. This class represents the common webhook protocol. The processing method of this class does not perform any other operations + * except storing the request and returning a general response. */ @Slf4j public class McpStandardProtocol implements Protocol { - public static final String PROTOCOL_NAME = "Mcp"; - public static final String PROTOCOL_VERSION = "2025-03-26"; + public static final String PROTOCOL_NAME = "MCP"; private SourceConnectorConfig sourceConnectorConfig; - private BlockingQueue queue; - // Getter methods - @Getter - private SessionManager sessionManager; - @Getter - private AIToolManager toolManager; - @Getter - private ObjectMapper objectMapper; /** - * 初始化协议处理器 + * Initialize the protocol + * + * @param sourceConnectorConfig source connector config */ @Override public void initialize(SourceConnectorConfig sourceConnectorConfig) { this.sourceConnectorConfig = sourceConnectorConfig; - this.sessionManager = new SessionManager(); - this.toolManager = new AIToolManager(); - this.objectMapper = new ObjectMapper(); - - log.info("McpStandardProtocol initialized - version: {}", PROTOCOL_VERSION); - } - - /** - * 初始化协议处理器(带队列) - */ - @Override - public void initialize(SourceConnectorConfig sourceConnectorConfig, BlockingQueue queue) { - this.queue = queue; - initialize(sourceConnectorConfig); } /** - * 创建 Netty 请求处理器 + * Set the handler for the route + * + * @param route route + * @param queue queue info */ @Override - public ChannelHandler createHandler(McpSourceConfig sourceConfig) { - if (sessionManager == null || toolManager == null || objectMapper == null) { - throw new IllegalStateException("Protocol not properly initialized"); - } - return new NettyMcpRequestHandler(sourceConfig, queue, sessionManager, objectMapper, toolManager); - } - - /** - * 设置 Vert.x 路由处理器(已弃用,保留兼容性) - */ - @Override - @Deprecated public void setHandler(Route route, BlockingQueue queue) { - log.warn("setHandler(Route, BlockingQueue) is deprecated in Netty implementation. Use createHandler(McpSourceConfig) instead."); - throw new UnsupportedOperationException("Vert.x Route handling is not supported in Netty implementation"); - } - - /** - * 转换消息为 ConnectRecord + route.method(HttpMethod.POST) + .handler(BodyHandler.create()) + .handler(ctx -> { + // Get the payload + Object payload = ctx.body().asString(Constants.DEFAULT_CHARSET.toString()); + payload = JsonUtils.parseObject(JsonUtils.toJSONString(payload), String.class); + + // Create and store the webhook request + Map headerMap = ctx.request().headers().entries().stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + McpRequest mcpRequest = new McpRequest(PROTOCOL_NAME, ctx.request().absoluteURI(), headerMap, payload, ctx); + if (!queue.offer(mcpRequest)) { + throw new IllegalStateException("Failed to store the request."); + } + + if (!sourceConnectorConfig.isDataConsistencyEnabled()) { + // Return 200 OK + ctx.response() + .setStatusCode(HttpResponseStatus.OK.code()) + .end(McpResponse.success().toJsonStr()); + } + + }) + .failureHandler(ctx -> { + log.error("Failed to handle the request. ", ctx.failure()); + + // Return Bad Response + ctx.response() + .setStatusCode(ctx.statusCode()) + .end(McpResponse.base(ctx.failure().getMessage()).toJsonStr()); + }); + + } + + /** + * Convert the message to a connect record + * + * @param message message + * @return connect record */ @Override public ConnectRecord convertToConnectRecord(Object message) { - if (!(message instanceof McpRequest)) { - log.warn("Received non-McpRequest message: {}", message != null ? message.getClass().getSimpleName() : "null"); - return null; - } - McpRequest request = (McpRequest) message; - - try { - // 确保会话存在 - ensureSessionExists(request); - - // 验证请求基本格式 - validateRequestFormat(request); - - // 提取数据 - Object data = extractDataFromRequest(request); - - // 创建 ConnectRecord - ConnectRecord connectRecord = new ConnectRecord( - null, // partition - null, // offset - System.currentTimeMillis(), // timestamp - data // data - ); - - // 添加协议相关扩展信息 - enrichConnectRecord(connectRecord, request); - - log.debug("Converted McpRequest to ConnectRecord: sessionId={}, recordId={}", - request.getSessionId(), connectRecord.getRecordId()); - - return connectRecord; - - } catch (Exception e) { - log.error("Failed to convert McpRequest to ConnectRecord for session: {}", - request.getSessionId(), e); - return null; - } - } - - /** - * 确保会话存在 - */ - private void ensureSessionExists(McpRequest request) { - String sessionId = request.getSessionId(); - MCPSession session = request.getSession(); - - // 如果请求中已经有会话对象,直接使用 - if (session != null) { - sessionManager.putSession(session); - return; - } - - // 如果有会话ID,尝试获取或创建会话 - if (sessionId != null && !sessionId.trim().isEmpty()) { - session = sessionManager.getSession(sessionId); - if (session == null) { - session = sessionManager.createSessionWithId(sessionId); - log.info("Created new session with provided ID: {}", sessionId); + ConnectRecord connectRecord = new ConnectRecord(null, null, System.currentTimeMillis(), request.getInputs()); + connectRecord.addExtension("protocol", PROTOCOL_NAME); + connectRecord.addExtension("session_id", request.getSessionId()); + request.getMetadata().forEach((k, v) -> { + if (k.equalsIgnoreCase("extension")) { + JsonObject extension = new JsonObject(v); + extension.forEach(e -> connectRecord.addExtension(e.getKey(), e.getValue())); } - request.setSession(session); - } else { - // 如果没有会话ID,创建新会话 - session = sessionManager.createSession(); - request.setSessionId(session.getSessionId()); - request.setSession(session); - log.info("Created new session: {}", session.getSessionId()); - } - } - - /** - * 验证请求格式 - */ - private void validateRequestFormat(McpRequest request) { - if (request.getPayloadMap() == null) { - throw new IllegalArgumentException("Request payload cannot be null"); - } - - // 验证协议版本(可选) - String protocolVersion = request.getProtocolVersion(); - if (protocolVersion != null && !PROTOCOL_VERSION.equals(protocolVersion)) { - log.warn("Protocol version mismatch: expected {}, got {}", PROTOCOL_VERSION, protocolVersion); - } - - // 验证基本字段 - Map payload = request.getPayloadMap(); - if (!payload.containsKey("inputs") && !payload.containsKey("tools")) { - log.warn("Request payload missing expected fields (inputs or tools)"); - } - } - - /** - * 从请求中提取数据 - */ - private Object extractDataFromRequest(McpRequest request) { - Map payload = request.getPayloadMap(); - - // 优先使用 inputs 作为主要数据 - Object inputs = payload.get("inputs"); - if (inputs != null) { - return inputs; - } + }); - // 如果没有 inputs,检查是否有 tools 字段 - Object tools = payload.get("tools"); - if (tools != null) { - return tools; - } - - // 如果都没有,使用整个 payload - return payload; - } - - /** - * 丰富 ConnectRecord 的扩展信息(使用CloudEvents兼容的方式) - */ - private void enrichConnectRecord(ConnectRecord connectRecord, McpRequest request) { - // 添加MCP标准扩展属性 - CloudEventsExtensionUtil.addMcpStandardExtensions( - connectRecord, - request.getSessionId(), - PROTOCOL_VERSION, - request.isStreaming() - ); - - // 添加基本信息 - CloudEventsExtensionUtil.addExtensionSafely(connectRecord, "source", "mcpconnector"); - CloudEventsExtensionUtil.addExtensionSafely(connectRecord, "requesturi", request.getAbsoluteURI()); - - // 添加会话信息 - MCPSession session = request.getSession(); - if (session != null) { - CloudEventsExtensionUtil.addExtensionSafely(connectRecord, "sessioncreated", String.valueOf(session.getLastActivity())); - Object context = session.getContext("context"); - if (context != null) { - CloudEventsExtensionUtil.addExtensionSafely(connectRecord, "sessioncontext", context.toString()); + // check data + if (connectRecord.getExtensionObj("isBase64") != null) { + if (Boolean.parseBoolean(connectRecord.getExtensionObj("isBase64").toString())) { + byte[] data = Base64.getDecoder().decode(connectRecord.getData().toString()); + connectRecord.setData(data); } } - - // 添加请求头信息 - Map headers = request.getHeaderMap(); - if (headers != null) { - CloudEventsExtensionUtil.addHeaderExtensions(connectRecord, headers); - } - - // 添加Payload元数据 - Map payload = request.getPayloadMap(); - if (payload != null) { - // 添加context信息 - Object context = payload.get("context"); - if (context != null) { - CloudEventsExtensionUtil.addExtensionSafely(connectRecord, "payloadcontext", context.toString()); - } - - // 添加metadata信息 - Object metadataObj = payload.get("metadata"); - if (metadataObj instanceof Map) { - Map metadata = (Map) metadataObj; - CloudEventsExtensionUtil.addMetadataExtensions(connectRecord, metadata); - } - - // 添加流式标识 - Object stream = payload.get("stream"); - if (stream != null) { - CloudEventsExtensionUtil.addExtensionSafely(connectRecord, "streamrequest", stream.toString()); - } + if (request.getRoutingContext() != null) { + connectRecord.addExtension("routingContext", request.getRoutingContext()); } - - // 生成唯一记录ID - String recordUniqueId = generateRecordUniqueId(request); - CloudEventsExtensionUtil.addExtensionSafely(connectRecord, "recordid", recordUniqueId); - - // 添加处理提示 - String processingHint = getProcessingHint(request); - CloudEventsExtensionUtil.addExtensionSafely(connectRecord, "processhint", processingHint); - - log.debug("Enriched ConnectRecord with CloudEvents-compatible extensions for session: {}", request.getSessionId()); - } - - /** - * 生成唯一记录ID - */ - private String generateRecordUniqueId(McpRequest request) { - return String.format("%s_%s_%d", - PROTOCOL_NAME, - request.getSessionId(), - System.currentTimeMillis()); - } - - /** - * 获取处理提示 - */ - private String getProcessingHint(McpRequest request) { - if (request.isStreaming()) { - return "streaming"; - } - - Map payload = request.getPayloadMap(); - if (payload != null && payload.containsKey("metadata")) { - Object metadataObj = payload.get("metadata"); - if (metadataObj instanceof Map) { - Map metadata = (Map) metadataObj; - Object priority = metadata.get("priority"); - if (priority != null) { - return "priority:" + priority; - } - } - } - - return "normal"; - } - - /** - * 提交记录 - */ - @Override - public void commit(ConnectRecord record) { - try { - String sessionId = (String) record.getExtensionObj("sessionId"); - if (sessionId != null && sessionManager != null) { - MCPSession session = sessionManager.getSession(sessionId); - if (session != null) { - session.updateLastActivity(); - } - } - - log.debug("Committed record: {}", record.getRecordId()); - - } catch (Exception e) { - log.error("Failed to commit record: {}", record.getRecordId(), e); - } - } - - /** - * 处理异常 - */ - @Override - public void onException(ConnectRecord record) { - try { - String sessionId = (String) record.getExtensionObj("sessionId"); - log.error("Exception occurred for record: {}, sessionId: {}", record.getRecordId(), sessionId); - - // 记录错误统计或执行恢复逻辑 - - } catch (Exception e) { - log.error("Failed to handle exception for record: {}", record.getRecordId(), e); - } - } - - /** - * 关闭协议处理器 - */ - @Override - public void shutdown() { - try { - log.info("Shutting down McpStandardProtocol..."); - - // 关闭会话管理器 - if (sessionManager != null) { - sessionManager.shutdown(); - } - - log.info("McpStandardProtocol shutdown completed"); - - } catch (Exception e) { - log.error("Error during protocol shutdown", e); - } - } - - /** - * 获取协议统计信息 - */ - public Map getProtocolStats() { - Map stats = new HashMap<>(); - stats.put("protocolName", PROTOCOL_NAME); - stats.put("protocolVersion", PROTOCOL_VERSION); - - // 添加会话统计 - if (sessionManager != null) { - // 可以添加会话数量等统计信息 - stats.put("sessionManagerInitialized", true); - } - - return stats; - } - - /** - * 创建成功响应 - */ - public String createSuccessResponse(Object data, String sessionId) { - try { - Map response = new HashMap<>(); - response.put("session_id", sessionId); - response.put("outputs", data); - - Map metadata = new HashMap<>(); - metadata.put("timestamp", System.currentTimeMillis()); - metadata.put("version", PROTOCOL_VERSION); - metadata.put("status", "success"); - response.put("metadata", metadata); - - return objectMapper.writeValueAsString(response); - - } catch (Exception e) { - log.error("Failed to create success response", e); - return createErrorResponse("Failed to create response", sessionId); - } - } - - /** - * 创建错误响应 - */ - public String createErrorResponse(String errorMessage, String sessionId) { - try { - Map response = new HashMap<>(); - response.put("session_id", sessionId); - response.put("error", Map.of( - "message", errorMessage, - "code", "PROCESSING_ERROR" - )); - - Map metadata = new HashMap<>(); - metadata.put("timestamp", System.currentTimeMillis()); - metadata.put("version", PROTOCOL_VERSION); - metadata.put("status", "error"); - response.put("metadata", metadata); - - return objectMapper.writeValueAsString(response); - - } catch (Exception e) { - log.error("Failed to create error response", e); - return "{\"error\":\"Internal server error\"}"; - } - } - - @Override - public String getProtocolName() { - return PROTOCOL_NAME; - } - - @Override - public String getProtocolVersion() { - return PROTOCOL_VERSION; - } - - @Override - public boolean supportsStreaming() { - return true; - } - - @Override - public boolean supportsCompression() { - return false; - } - - @Override - public Map getProtocolConfig() { - return getProtocolStats(); - } - - @Override - public boolean validateConfig(SourceConnectorConfig config) { - return config != null; + return connectRecord; } -} \ No newline at end of file +} diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/AIToolManager.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/AIToolManager.java deleted file mode 100644 index a27e50270e..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/AIToolManager.java +++ /dev/null @@ -1,162 +0,0 @@ -package org.apache.eventmesh.connector.mcp.util; - -import org.apache.eventmesh.connector.mcp.session.MCPSession; -import lombok.extern.slf4j.Slf4j; -import org.apache.eventmesh.connector.mcp.source.data.MCPStreamingResponse; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; - -/** - * AI工具管理器 - * 负责处理AI相关的请求和流式响应 - */ -@Slf4j -public class AIToolManager { - - /** - * 处理普通请求 - * - * @param userContent 用户输入内容 - * @param session MCP会话 - * @param metadata 元数据 - * @return 处理结果的Future - */ - public CompletableFuture processRequest(String userContent, MCPSession session, Map metadata) { - return CompletableFuture.supplyAsync(() -> { - try { - log.info("Processing request for session: {}, content: {}", session.getSessionId(), userContent); - - // 模拟AI处理 - String response = "Based on your input: '" + userContent + "', here's my response. "; - - // 考虑会话上下文 - List history = (List) session.getContext("history"); - if (history != null && !history.isEmpty()) { - response += "I can see from our conversation history that we've discussed: " + String.join(", ", history) + ". "; - } - - response += "This is a comprehensive response to your query."; - - // 模拟处理时间 - Thread.sleep(500); - - log.info("Request processed successfully for session: {}", session.getSessionId()); - return response; - - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.error("Request processing interrupted for session: {}", session.getSessionId(), e); - return "Processing was interrupted."; - } catch (Exception e) { - log.error("Error processing request for session: {}", session.getSessionId(), e); - return "Error occurred while processing your request."; - } - }); - } - - /** - * 处理流式请求 - * - * @param userContent 用户输入内容 - * @param session MCP会话 - * @param metadata 元数据 - * @return 流式结果的Future - */ - public CompletableFuture processStreamingRequest(String userContent, MCPSession session, Map metadata) { - return CompletableFuture.supplyAsync(() -> { - try { - log.info("Processing streaming request for session: {}, content: {}", session.getSessionId(), userContent); - - // 模拟AI流式处理 - String response = "正在分析您的输入: " + userContent + " 生成回复中... 这是一个很好的问题。 让我从几个角度来分析: 首先,技术层面的考虑... 其次,实际应用场景... 然后,未来发展趋势... 最后,我的具体建议... 综合来看,我认为... 希望这个回答对您有帮助!"; - - // 考虑会话上下文 - List history = (List) session.getContext("history"); - if (history != null && !history.isEmpty()) { - response += " 结合我们之前的对话历史,我认为... 这与之前讨论的" + String.join("、", history) + "相呼应。"; - } - - // 从metadata中获取额外信息 - if (metadata != null) { - String userId = (String) metadata.get("user_id"); - if (userId != null) { - response += " 针对用户" + userId + "的个性化建议..."; - } - } - - log.info("Streaming request processed successfully for session: {}", session.getSessionId()); - return new MCPStreamingResponse(response); - - } catch (Exception e) { - log.error("Error processing streaming request for session: {}", session.getSessionId(), e); - return new MCPStreamingResponse("Error occurred while processing your streaming request."); - } - }); - } - - /** - * 处理工具调用请求 - * - * @param toolName 工具名称 - * @param input 输入参数 - * @param session MCP会话 - * @return 处理结果的Future - */ - public CompletableFuture callTool(String toolName, String input, MCPSession session) { - return CompletableFuture.supplyAsync(() -> { - try { - log.info("Calling tool: {} for session: {}", toolName, session.getSessionId()); - - switch (toolName) { - case "text-generator": - return generateText(input, session); - case "code-analyzer": - return analyzeCode(input, session); - case "data-processor": - return processData(input, session); - default: - log.warn("Unknown tool: {}", toolName); - return "Unknown tool: " + toolName; - } - - } catch (Exception e) { - log.error("Error calling tool: {} for session: {}", toolName, session.getSessionId(), e); - return "Error occurred while calling tool: " + toolName; - } - }); - } - - /** - * 文本生成工具 - */ - private String generateText(String input, MCPSession session) { - // 模拟文本生成 - return "Generated text based on input: " + input + ". Session: " + session.getSessionId(); - } - - /** - * 代码分析工具 - */ - private String analyzeCode(String input, MCPSession session) { - // 模拟代码分析 - return "Code analysis result for: " + input + - "\n- Syntax: OK" + - "\n- Performance: Good" + - "\n- Security: No issues found" + - "\n- Session: " + session.getSessionId(); - } - - /** - * 数据处理工具 - */ - private String processData(String input, MCPSession session) { - // 模拟数据处理 - return "Data processing result for: " + input + - "\n- Records processed: 1000" + - "\n- Success rate: 98.5%" + - "\n- Processing time: 2.3s" + - "\n- Session: " + session.getSessionId(); - } -} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/CloudEventsExtensionUtil.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/CloudEventsExtensionUtil.java deleted file mode 100644 index 3602847180..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/CloudEventsExtensionUtil.java +++ /dev/null @@ -1,213 +0,0 @@ -package org.apache.eventmesh.connector.mcp.util; - -import lombok.extern.slf4j.Slf4j; -import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; - -import java.util.Map; -import java.util.regex.Pattern; - -/** - * CloudEvents扩展属性工具类 - * - * 根据CloudEvents规范处理扩展属性: - * - 扩展名称必须是小写字母、数字和短横线的组合 - * - 必须以字母开头 - * - 长度在1-20个字符之间 - * - 不能包含点号、下划线等特殊字符 - */ -@Slf4j -public class CloudEventsExtensionUtil { - - // CloudEvents扩展名称的正则表达式 - 只允许小写字母和数字 - private static final Pattern VALID_EXTENSION_NAME = Pattern.compile("^[a-z][a-z0-9]{0,19}$"); - - /** - * 安全地添加扩展属性到ConnectRecord - * - * @param record ConnectRecord对象 - * @param name 扩展属性名称 - * @param value 扩展属性值 - */ - public static void addExtensionSafely(ConnectRecord record, String name, Object value) { - if (record == null || name == null) { - return; - } - - String safeName = normalizeExtensionName(name); - String safeValue = normalizeExtensionValue(value); - - try { - record.addExtension(safeName, safeValue); - log.debug("Added extension: {} = {}", safeName, safeValue); - } catch (Exception e) { - log.warn("Failed to add extension {}: {}", safeName, e.getMessage()); - } - } - - /** - * 批量添加扩展属性 - * - * @param record ConnectRecord对象 - * @param extensions 扩展属性映射 - * @param prefix 前缀 - */ - public static void addExtensionsBatch(ConnectRecord record, Map extensions, String prefix) { - if (record == null || extensions == null) { - return; - } - - String safePrefix = prefix != null ? normalizeExtensionName(prefix) : ""; - - extensions.forEach((key, value) -> { - String fullName = safePrefix + normalizeExtensionName(key); - addExtensionSafely(record, fullName, value); - }); - } - - /** - * 规范化扩展名称以符合CloudEvents规范 - * - * CloudEvents扩展名称规则(更严格): - * - 只能包含小写字母(a-z)和数字(0-9) - * - 必须以小写字母开头 - * - 长度在1-20个字符之间 - * - 不能包含短横线、下划线、点号等特殊字符 - * - * @param name 原始名称 - * @return 规范化后的名称 - */ - public static String normalizeExtensionName(String name) { - if (name == null || name.isEmpty()) { - return "unknown"; - } - - // 转换为小写 - String normalized = name.toLowerCase(); - - // 移除所有非字母数字字符 - normalized = normalized.replaceAll("[^a-z0-9]", ""); - - // 确保以字母开头 - if (normalized.isEmpty() || !Character.isLetter(normalized.charAt(0))) { - normalized = "ext" + normalized; - } - - // 限制长度为20个字符 - if (normalized.length() > 20) { - normalized = normalized.substring(0, 20); - } - - // 最终检查,确保不为空且有效 - if (normalized.isEmpty() || !isValidExtensionName(normalized)) { - normalized = "ext"; - } - - return normalized; - } - - /** - * 规范化扩展值 - * - * @param value 原始值 - * @return 规范化后的字符串值 - */ - public static String normalizeExtensionValue(Object value) { - if (value == null) { - return ""; - } - - String stringValue = value.toString(); - - // CloudEvents扩展值必须是字符串,且不能包含控制字符 - return stringValue.replaceAll("[\\p{Cntrl}]", " ").trim(); - } - - /** - * 验证扩展名称是否符合CloudEvents规范 - * - * @param name 扩展名称 - * @return 是否有效 - */ - public static boolean isValidExtensionName(String name) { - return name != null && VALID_EXTENSION_NAME.matcher(name).matches(); - } - - /** - * 创建MCP特定的扩展属性前缀 - * - * @param category 类别 - * @return 规范化的前缀 - */ - public static String createMcpPrefix(String category) { - return normalizeExtensionName("mcp" + category); - } - - /** - * 为MCP协议添加标准扩展属性 - * - * @param record ConnectRecord对象 - * @param sessionId 会话ID - * @param protocolVersion 协议版本 - * @param isStreaming 是否流式 - */ - public static void addMcpStandardExtensions(ConnectRecord record, String sessionId, - String protocolVersion, boolean isStreaming) { - addExtensionSafely(record, "mcpprotocol", "mcp"); - addExtensionSafely(record, "mcpversion", protocolVersion); - addExtensionSafely(record, "mcpsession", sessionId); - addExtensionSafely(record, "mcpstreaming", String.valueOf(isStreaming)); - addExtensionSafely(record, "mcptimestamp", String.valueOf(System.currentTimeMillis())); - } - - /** - * 为请求头添加扩展属性 - * - * @param record ConnectRecord对象 - * @param headers 请求头映射 - */ - public static void addHeaderExtensions(ConnectRecord record, Map headers) { - if (headers == null || headers.isEmpty()) { - return; - } - - headers.forEach((key, value) -> { - String extensionName = "header" + normalizeExtensionName(key); - addExtensionSafely(record, extensionName, value); - }); - } - - /** - * 为元数据添加扩展属性 - * - * @param record ConnectRecord对象 - * @param metadata 元数据映射 - */ - public static void addMetadataExtensions(ConnectRecord record, Map metadata) { - if (metadata == null || metadata.isEmpty()) { - return; - } - - metadata.forEach((key, value) -> { - String extensionName = "meta" + normalizeExtensionName(key); - addExtensionSafely(record, extensionName, value); - }); - } - - /** - * 获取所有有效的扩展名称示例(用于测试和文档) - * - * @return 有效扩展名称示例列表 - */ - public static String[] getValidExtensionExamples() { - return new String[]{ - "mcpprotocol", - "mcpversion", - "mcpsession", - "mcpstreaming", - "headercontenttype", - "metauserid", - "requesttimestamp", - "processinghint" - }; - } -} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/McpResponseValidator.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/McpResponseValidator.java deleted file mode 100644 index 24ec512478..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/util/McpResponseValidator.java +++ /dev/null @@ -1,311 +0,0 @@ -package org.apache.eventmesh.connector.mcp.util; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.extern.slf4j.Slf4j; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * MCP响应格式验证器 - * 确保生成的响应完全符合MCP 2025-03-26规范 - */ -@Slf4j -public class McpResponseValidator { - - private static final ObjectMapper objectMapper = new ObjectMapper(); - private static final String PROTOCOL_VERSION = "2025-03-26"; - - /** - * 创建标准的MCP流式响应 - * - * @param sessionId 会话ID - * @param content 内容 - * @param chunkIndex 块索引 - * @param isComplete 是否完成 - * @param protocolVersion 协议版本 - * @return 格式化的响应JSON字符串 - */ - public static String createStreamingResponse(String sessionId, String content, - int chunkIndex, boolean isComplete, - String protocolVersion) { - try { - Map response = new LinkedHashMap<>(); - - // outputs 数组 - 必须字段 - Map output = new HashMap<>(); - output.put("role", "assistant"); - output.put("content", sanitizeContent(content)); - response.put("outputs", Arrays.asList(output)); - - // metadata 对象 - 必须字段 - Map metadata = new LinkedHashMap<>(); - metadata.put("chunk_index", chunkIndex); - metadata.put("streaming", true); - metadata.put("is_complete", isComplete); - metadata.put("version", protocolVersion != null ? protocolVersion : PROTOCOL_VERSION); - metadata.put("timestamp", System.currentTimeMillis()); - response.put("metadata", metadata); - - // session_id - 必须字段 - response.put("session_id", sessionId); - - String jsonString = objectMapper.writeValueAsString(response); - - // 验证生成的JSON - if (!isValidMcpResponse(jsonString)) { - log.error("Generated invalid MCP response"); - return createFallbackResponse(sessionId, chunkIndex, isComplete); - } - - return jsonString; - - } catch (Exception e) { - log.error("Error creating streaming response", e); - return createFallbackResponse(sessionId, chunkIndex, isComplete); - } - } - - /** - * 创建标准的MCP错误响应 - * - * @param sessionId 会话ID - * @param errorMessage 错误消息 - * @param errorCode 错误代码 - * @param protocolVersion 协议版本 - * @return 格式化的错误响应JSON字符串 - */ - public static String createErrorResponse(String sessionId, String errorMessage, - String errorCode, String protocolVersion) { - try { - Map response = new LinkedHashMap<>(); - - // error 对象 - 必须字段 - Map error = new HashMap<>(); - error.put("message", sanitizeContent(errorMessage)); - error.put("code", errorCode != null ? errorCode : "UNKNOWN_ERROR"); - response.put("error", error); - - // metadata 对象 - Map metadata = new LinkedHashMap<>(); - metadata.put("streaming", false); - metadata.put("version", protocolVersion != null ? protocolVersion : PROTOCOL_VERSION); - metadata.put("timestamp", System.currentTimeMillis()); - response.put("metadata", metadata); - - // session_id - response.put("session_id", sessionId); - - return objectMapper.writeValueAsString(response); - - } catch (Exception e) { - log.error("Error creating error response", e); - return "{\"error\":{\"message\":\"Internal server error\",\"code\":\"INTERNAL_ERROR\"},\"session_id\":\"" + sessionId + "\"}"; - } - } - - /** - * 创建标准的MCP成功响应 - * - * @param sessionId 会话ID - * @param content 响应内容 - * @param protocolVersion 协议版本 - * @return 格式化的成功响应JSON字符串 - */ - public static String createSuccessResponse(String sessionId, String content, String protocolVersion) { - try { - Map response = new LinkedHashMap<>(); - - // outputs 数组 - Map output = new HashMap<>(); - output.put("role", "assistant"); - output.put("content", sanitizeContent(content)); - response.put("outputs", Arrays.asList(output)); - - // metadata 对象 - Map metadata = new LinkedHashMap<>(); - metadata.put("streaming", false); - metadata.put("version", protocolVersion != null ? protocolVersion : PROTOCOL_VERSION); - metadata.put("timestamp", System.currentTimeMillis()); - response.put("metadata", metadata); - - // session_id - response.put("session_id", sessionId); - - return objectMapper.writeValueAsString(response); - - } catch (Exception e) { - log.error("Error creating success response", e); - return createErrorResponse(sessionId, "Failed to create response", "RESPONSE_ERROR", protocolVersion); - } - } - - /** - * 验证MCP响应格式是否正确 - * - * @param jsonString JSON字符串 - * @return 是否有效 - */ - public static boolean isValidMcpResponse(String jsonString) { - try { - JsonNode root = objectMapper.readTree(jsonString); - - // 检查必须字段 - if (!root.has("session_id")) { - log.warn("Missing session_id field"); - return false; - } - - // 检查是否有error或outputs - boolean hasError = root.has("error"); - boolean hasOutputs = root.has("outputs"); - - if (!hasError && !hasOutputs) { - log.warn("Missing both error and outputs fields"); - return false; - } - - // 检查metadata - if (!root.has("metadata")) { - log.warn("Missing metadata field"); - return false; - } - - JsonNode metadata = root.get("metadata"); - if (!metadata.has("version") || !metadata.has("timestamp")) { - log.warn("Missing required metadata fields"); - return false; - } - - // 如果是流式响应,检查流式字段 - if (metadata.has("streaming") && metadata.get("streaming").asBoolean()) { - if (!metadata.has("chunk_index") || !metadata.has("is_complete")) { - log.warn("Missing required streaming metadata fields"); - return false; - } - } - - return true; - - } catch (Exception e) { - log.warn("JSON parsing error: {}", e.getMessage()); - return false; - } - } - - /** - * 清理内容,确保JSON安全 - * - * @param content 原始内容 - * @return 清理后的内容 - */ - private static String sanitizeContent(String content) { - if (content == null) { - return ""; - } - - // 移除可能导致JSON问题的字符 - return content.trim() - .replaceAll("[\r\n\t]", " ") // 替换换行和制表符 - .replaceAll("\\s+", " ") // 合并多个空格 - .replaceAll("\"", "\\\""); // 转义双引号 - } - - /** - * 创建备用响应 - * - * @param sessionId 会话ID - * @param chunkIndex 块索引 - * @param isComplete 是否完成 - * @return 备用响应JSON字符串 - */ - private static String createFallbackResponse(String sessionId, int chunkIndex, boolean isComplete) { - return String.format( - "{\"outputs\":[{\"role\":\"assistant\",\"content\":\"Error processing content\"}]," + - "\"metadata\":{\"chunk_index\":%d,\"streaming\":true,\"is_complete\":%s,\"version\":\"%s\",\"timestamp\":%d}," + - "\"session_id\":\"%s\"}", - chunkIndex, isComplete, PROTOCOL_VERSION, System.currentTimeMillis(), sessionId - ); - } - - /** - * 格式化JSON字符串(用于调试) - * - * @param jsonString JSON字符串 - * @return 格式化后的JSON字符串 - */ - public static String formatJson(String jsonString) { - try { - JsonNode jsonNode = objectMapper.readTree(jsonString); - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode); - } catch (Exception e) { - log.warn("Failed to format JSON: {}", e.getMessage()); - return jsonString; - } - } - - /** - * 验证并修复JSON字符串 - * - * @param jsonString 原始JSON字符串 - * @param sessionId 会话ID(用于修复) - * @return 修复后的JSON字符串 - */ - public static String validateAndFix(String jsonString, String sessionId) { - if (isValidMcpResponse(jsonString)) { - return jsonString; - } - - log.warn("Invalid MCP response detected, attempting to fix"); - - try { - JsonNode root = objectMapper.readTree(jsonString); - Map fixed = new LinkedHashMap<>(); - - // 确保有session_id - if (root.has("session_id")) { - fixed.put("session_id", root.get("session_id").asText()); - } else { - fixed.put("session_id", sessionId); - } - - // 处理outputs或error - if (root.has("outputs")) { - fixed.put("outputs", objectMapper.convertValue(root.get("outputs"), Object.class)); - } else if (root.has("error")) { - fixed.put("error", objectMapper.convertValue(root.get("error"), Object.class)); - } else { - Map error = new HashMap<>(); - error.put("message", "Unknown error"); - error.put("code", "UNKNOWN_ERROR"); - fixed.put("error", error); - } - - // 确保有完整的metadata - Map metadata = new LinkedHashMap<>(); - if (root.has("metadata")) { - JsonNode metaNode = root.get("metadata"); - metadata.putAll(objectMapper.convertValue(metaNode, Map.class)); - } - - // 补充缺失的metadata字段 - if (!metadata.containsKey("version")) { - metadata.put("version", PROTOCOL_VERSION); - } - if (!metadata.containsKey("timestamp")) { - metadata.put("timestamp", System.currentTimeMillis()); - } - - fixed.put("metadata", metadata); - - return objectMapper.writeValueAsString(fixed); - - } catch (Exception e) { - log.error("Failed to fix JSON: {}", e.getMessage()); - return createErrorResponse(sessionId, "JSON format error", "JSON_ERROR", PROTOCOL_VERSION); - } - } -} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/META-INF.eventmesh/org.apache.eventmesh.openconnect.api.ConnectorCreateService b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/META-INF.eventmesh/org.apache.eventmesh.openconnect.api.ConnectorCreateService index bde014808a..01a46e9de0 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/META-INF.eventmesh/org.apache.eventmesh.openconnect.api.ConnectorCreateService +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/META-INF.eventmesh/org.apache.eventmesh.openconnect.api.ConnectorCreateService @@ -17,3 +17,4 @@ MCP-Source=org.apache.eventmesh.connector.mcp.source.McpSourceConnector +MCP-Sink=org.apache.eventmesh.connector.mcp.sink.McpSinkConnector \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/server-config.yml b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/server-config.yml index 0cd7b5b5ab..5f66dd0f68 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/server-config.yml +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/server-config.yml @@ -16,4 +16,4 @@ # sourceEnable: true -sinkEnable: false +sinkEnable: true diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/sink-config.yml b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/sink-config.yml new file mode 100644 index 0000000000..c04f886699 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/sink-config.yml @@ -0,0 +1,56 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +pubSubConfig: + meshAddress: 127.0.0.1:10000 + subject: TopicTest + idc: FT + env: PRD + group: mcpSink + appId: 5032 + userName: mcpSourceUser + passWord: mcpPassWord +connectorConfig: + connectorName: mcpSink + urls: + - http://127.0.0.1:7092/test + keepAlive: true + keepAliveTimeout: 60000 + idleTimeout: 5000 # timeunit: ms, recommended scope: common(5s - 10s), webhook(15s - 60s) + connectionTimeout: 5000 # timeunit: ms, recommended scope: 5 - 10s + maxConnectionPoolSize: 5 + retryConfig: + maxRetries: 2 + interval: 1000 + retryOnNonSuccess: false diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/source-config.yml b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/source-config.yml index eb8cc07d02..4bd63254a7 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/source-config.yml +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/source-config.yml @@ -20,10 +20,10 @@ pubSubConfig: subject: TopicTest idc: FT env: PRD - group: httpSource + group: mcpSource appId: 5032 - userName: httpSourceUser - passWord: httpPassWord + userName: mcpSourceUser + passWord: mcpPassWord connectorConfig: connectorName: mcpSource path: /test @@ -34,9 +34,4 @@ connectorConfig: extraConfig: # extra config for different protocol, e.g. GitHub secret streamType: chunked contentType: application/json - reconnection: true -sessionConfig: - sessionStorage: redis - redisHost: 127.0.0.1 - redisPort: 6379 - sessionTimeout: 10m \ No newline at end of file + reconnection: true \ No newline at end of file diff --git a/eventmesh-examples/src/main/java/org/apache/eventmesh/http/demo/sub/RemoteSubscribeInstance.java b/eventmesh-examples/src/main/java/org/apache/eventmesh/http/demo/sub/RemoteSubscribeInstance.java index 99837d27c4..1cfba31579 100644 --- a/eventmesh-examples/src/main/java/org/apache/eventmesh/http/demo/sub/RemoteSubscribeInstance.java +++ b/eventmesh-examples/src/main/java/org/apache/eventmesh/http/demo/sub/RemoteSubscribeInstance.java @@ -32,23 +32,63 @@ import org.apache.eventmesh.common.utils.IPUtils; import org.apache.eventmesh.common.utils.JsonUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import io.netty.handler.codec.http.HttpMethod; +import org.apache.http.util.EntityUtils; public class RemoteSubscribeInstance { static final CloseableHttpClient httpClient = HttpClients.createDefault(); - public static void main(String[] args) { - subscribeRemote(); + public static void main(String[] args) throws IOException { + subscribeLocal(); + //subscribeRemote(); // unsubscribeRemote(); } + private static void subscribeLocal() throws IOException { + SubscriptionItem item = new SubscriptionItem(); + item.setTopic(ExampleConstants.EVENTMESH_HTTP_ASYNC_TEST_TOPIC); + item.setMode(SubscriptionMode.CLUSTERING); + item.setType(SubscriptionType.ASYNC); + + Map body = new HashMap<>(); + body.put("url", "http://127.0.0.1:8088/sub/test"); + body.put("consumerGroup", "EventMeshTest-consumerGroup"); + body.put("topic", Collections.singletonList(item)); + + String json = JsonUtils.toJSONString(body); + // 2) 直接用 HttpPost + HttpPost post = new HttpPost("http://127.0.0.1:10105/eventmesh/subscribe/local"); + post.setHeader("Content-Type", "application/json"); + post.setHeader("env", "prod"); + post.setHeader("idc", "default"); + post.setHeader("sys", "http-client-demo"); + post.setHeader("username", "eventmesh"); + post.setHeader("passwd", "eventmesh"); + post.setHeader("ip", IPUtils.getLocalAddress()); + post.setHeader("language", "JAVA"); + post.setEntity(new StringEntity(json, StandardCharsets.UTF_8)); + + try (CloseableHttpResponse resp = httpClient.execute(post)) { + String respBody = EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8); + System.out.println(resp.getStatusLine()); + System.out.println(respBody); + } + } + private static void subscribeRemote() { SubscriptionItem subscriptionItem = new SubscriptionItem(); subscriptionItem.setTopic(ExampleConstants.EVENTMESH_HTTP_ASYNC_TEST_TOPIC); diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/gradle.porperties b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/gradle.porperties deleted file mode 100644 index 83a61e1720..0000000000 --- a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/gradle.porperties +++ /dev/null @@ -1,18 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You 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. -# - -pluginType=protocol -pluginName=mcp \ No newline at end of file diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolAdaptor.java b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolAdaptor.java deleted file mode 100644 index bec704046e..0000000000 --- a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolAdaptor.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.apache.eventmesh.protocol.mcp; - -import com.fasterxml.jackson.core.type.TypeReference; -import io.cloudevents.CloudEvent; -import org.apache.eventmesh.common.Constants; -import org.apache.eventmesh.common.protocol.ProtocolTransportObject; -import org.apache.eventmesh.common.utils.JsonUtils; -import org.apache.eventmesh.protocol.api.ProtocolAdaptor; -import org.apache.eventmesh.protocol.api.exception.ProtocolHandleException; -import org.apache.eventmesh.protocol.mcp.resolver.McpRequestProtocolResolver; -import org.apache.eventmesh.common.protocol.mcp.McpEventWrapper; - -import java.nio.charset.StandardCharsets; -import java.util.*; - -import static org.apache.eventmesh.protocol.mcp.McpProtocolConstant.CONSTANTS_KEY_BODY; -import static org.apache.eventmesh.protocol.mcp.McpProtocolConstant.CONSTANTS_KEY_HEADERS; - -public class McpProtocolAdaptor - implements ProtocolAdaptor { - - @Override - public CloudEvent toCloudEvent(ProtocolTransportObject protocolTransportObject) throws ProtocolHandleException { - if (protocolTransportObject instanceof McpEventWrapper) { - McpEventWrapper wrapper = (McpEventWrapper) protocolTransportObject; - return McpRequestProtocolResolver.buildEvent(wrapper); - } else { - throw new ProtocolHandleException("Unsupported protocol: " + protocolTransportObject.getClass()); - } - } - - @Override - public List toBatchCloudEvent(ProtocolTransportObject protocol) throws ProtocolHandleException { - return Collections.emptyList(); // 可支持批处理扩展 - } - - @Override - public ProtocolTransportObject fromCloudEvent(CloudEvent cloudEvent) throws ProtocolHandleException { - McpEventWrapper wrapper = new McpEventWrapper(); - - Map sysHeaderMap = new HashMap<>(); - for (String attr : cloudEvent.getAttributeNames()) { - sysHeaderMap.put(attr, cloudEvent.getAttribute(attr)); - } - for (String ext : cloudEvent.getExtensionNames()) { - sysHeaderMap.put(ext, cloudEvent.getExtension(ext)); - } - wrapper.setSysHeaderMap(sysHeaderMap); - - if (cloudEvent.getData() != null) { - Map dataContentMap = JsonUtils.parseTypeReferenceObject( - new String(Objects.requireNonNull(cloudEvent.getData()).toBytes(), Constants.DEFAULT_CHARSET), - new TypeReference>() {} - ); - - String rawHeader = JsonUtils.toJSONString(dataContentMap.get(CONSTANTS_KEY_HEADERS)); - byte[] body = JsonUtils.toJSONString(dataContentMap.get(CONSTANTS_KEY_BODY)).getBytes(StandardCharsets.UTF_8); - - Map headerMap = JsonUtils.parseTypeReferenceObject( - rawHeader, new TypeReference>() {} - ); - - wrapper.setHeaderMap(headerMap); - wrapper.setBody(body); - } - - return wrapper; - } - - @Override - public String getProtocolType() { - return McpProtocolConstant.PROTOCOL_NAME; - } -} diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolConstant.java b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolConstant.java deleted file mode 100644 index 0b31515b57..0000000000 --- a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/McpProtocolConstant.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.eventmesh.protocol.mcp; - -public enum McpProtocolConstant { - ; - - public static final String PROTOCOL_NAME = "mcp"; - - public static final String CONSTANTS_KEY_ID = "id"; - public static final String CONSTANTS_KEY_SOURCE = "source"; - public static final String CONSTANTS_KEY_TYPE = "type"; - public static final String CONSTANTS_KEY_SUBJECT = "subject"; - public static final String CONSTANTS_KEY_BODY = "body"; - public static final String CONSTANTS_KEY_HEADERS = "headers"; - public static final String CONSTANTS_KEY_TIMESTAMP = "timestamp"; - public static final String CONSTANTS_KEY_SCHEMA = "schema"; - public static final String CONSTANTS_KEY_CONTEXT = "context"; - - public static final String CONSTANTS_HEADER_SESSION_ID = "Mcp-Session-Id"; - public static final String CONSTANTS_HEADER_STREAM_ID = "Mcp-Stream-Id"; - - public static final String CONSTANTS_DEFAULT_TYPE = "mcp_request"; - public static final String CONSTANTS_DEFAULT_SOURCE = "/"; - public static final String CONSTANTS_DEFAULT_SUBJECT = ""; - - public static final String CONSTANTS_APPLICATION_JSON = "application/json"; - public static final String CONSTANTS_APPLICATION_PROTOBUF = "application/x-protobuf"; - public static final String CONSTANTS_DATA_CONTENT_TYPE = "Content-Type"; -} diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/resolver/McpRequestProtocolResolver.java b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/resolver/McpRequestProtocolResolver.java deleted file mode 100644 index 96fd5d877d..0000000000 --- a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/java/org/apache/eventmesh/protocol/mcp/resolver/McpRequestProtocolResolver.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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 - * - * Mcp://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.apache.eventmesh.protocol.mcp.resolver; - -import com.fasterxml.jackson.core.type.TypeReference; -import io.cloudevents.CloudEvent; -import io.cloudevents.core.v1.CloudEventBuilder; -import org.apache.commons.lang3.StringUtils; -import org.apache.eventmesh.common.utils.JsonUtils; -import org.apache.eventmesh.protocol.api.exception.ProtocolHandleException; -import org.apache.eventmesh.protocol.mcp.McpProtocolConstant; -import org.apache.eventmesh.common.protocol.mcp.McpEventWrapper; - -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.UUID; - -public class McpRequestProtocolResolver { - - public static CloudEvent buildEvent(McpEventWrapper mcpEventWrapper) throws ProtocolHandleException { - try { - CloudEventBuilder builder = new CloudEventBuilder(); - - Map requestHeaderMap = mcpEventWrapper.getHeaderMap(); - Map sysHeaderMap = mcpEventWrapper.getSysHeaderMap(); - - String id = sysHeaderMap.getOrDefault(McpProtocolConstant.CONSTANTS_KEY_ID, UUID.randomUUID()).toString(); - String source = sysHeaderMap.getOrDefault(McpProtocolConstant.CONSTANTS_KEY_SOURCE, - McpProtocolConstant.CONSTANTS_DEFAULT_SOURCE).toString(); - String type = sysHeaderMap.getOrDefault(McpProtocolConstant.CONSTANTS_KEY_TYPE, - McpProtocolConstant.CONSTANTS_DEFAULT_TYPE).toString(); - String subject = sysHeaderMap.getOrDefault(McpProtocolConstant.CONSTANTS_KEY_SUBJECT, - McpProtocolConstant.CONSTANTS_DEFAULT_SUBJECT).toString(); - - String dataContentType = requestHeaderMap.getOrDefault(McpProtocolConstant.CONSTANTS_DATA_CONTENT_TYPE, - McpProtocolConstant.CONSTANTS_APPLICATION_JSON).toString(); - - // Set basic CloudEvent attributes - builder.withId(id) - .withType(type) - .withSource(URI.create(source)) - .withSubject(subject) - .withDataContentType(dataContentType); - - // Set extensions - for (Map.Entry entry : sysHeaderMap.entrySet()) { - String key = entry.getKey(); - if (key.equalsIgnoreCase(McpProtocolConstant.CONSTANTS_KEY_ID) - || key.equalsIgnoreCase(McpProtocolConstant.CONSTANTS_KEY_SOURCE) - || key.equalsIgnoreCase(McpProtocolConstant.CONSTANTS_KEY_TYPE) - || key.equalsIgnoreCase(McpProtocolConstant.CONSTANTS_KEY_SUBJECT)) { - continue; - } - builder.withExtension(key.toLowerCase(Locale.ROOT), entry.getValue().toString()); - } - - // Handle body - byte[] requestBody = mcpEventWrapper.getBody(); - if (StringUtils.equals(dataContentType, McpProtocolConstant.CONSTANTS_APPLICATION_JSON)) { - Map requestBodyMap = JsonUtils.parseTypeReferenceObject( - new String(requestBody, StandardCharsets.UTF_8), - new TypeReference>() {} - ); - - Map data = new HashMap<>(); - data.put(McpProtocolConstant.CONSTANTS_KEY_HEADERS, requestHeaderMap); - data.put(McpProtocolConstant.CONSTANTS_KEY_BODY, requestBodyMap); - - builder.withData(JsonUtils.toJSONString(data).getBytes(StandardCharsets.UTF_8)); - } else { - builder.withData(requestBody); - } - - return builder.build(); - } catch (Exception e) { - throw new ProtocolHandleException("Failed to build CloudEvent from McpEventWrapper", e); - } - } - -} diff --git a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/resources/eventmesh/org.apache.eventmesh.protocol.mcp.ProtocolAdaptor b/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/resources/eventmesh/org.apache.eventmesh.protocol.mcp.ProtocolAdaptor deleted file mode 100644 index b086ff7083..0000000000 --- a/eventmesh-protocol-plugin/eventmesh-protocol-mcp/src/main/resources/eventmesh/org.apache.eventmesh.protocol.mcp.ProtocolAdaptor +++ /dev/null @@ -1,16 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You 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. - -mcp=org.apache.eventmesh.protocol.mcp.McpProtocolAdaptor \ No newline at end of file diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/AbstractMCPServer.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/AbstractMCPServer.java new file mode 100644 index 0000000000..cd3126eb32 --- /dev/null +++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/AbstractMCPServer.java @@ -0,0 +1,524 @@ +package org.apache.eventmesh.runtime.boot; + +import org.apache.eventmesh.common.ThreadPoolFactory; +import org.apache.eventmesh.common.config.CommonConfiguration; +import org.apache.eventmesh.common.protocol.http.HttpCommand; +import org.apache.eventmesh.common.protocol.http.body.Body; +import org.apache.eventmesh.common.protocol.http.common.EventMeshRetCode; +import org.apache.eventmesh.common.protocol.http.common.ProtocolKey; +import org.apache.eventmesh.common.protocol.http.common.ProtocolVersion; +import org.apache.eventmesh.common.protocol.http.common.RequestCode; +import org.apache.eventmesh.common.protocol.http.header.Header; +import org.apache.eventmesh.common.utils.AssertUtils; +import org.apache.eventmesh.runtime.configuration.EventMeshHTTPConfiguration; +import org.apache.eventmesh.runtime.constants.EventMeshConstants; +import org.apache.eventmesh.runtime.core.protocol.http.async.AsyncContext; +import org.apache.eventmesh.runtime.core.protocol.http.processor.HandlerService; +import org.apache.eventmesh.runtime.core.protocol.http.processor.inf.HttpRequestProcessor; +import org.apache.eventmesh.runtime.metrics.http.EventMeshHttpMetricsManager; +import org.apache.eventmesh.runtime.util.HttpRequestUtil; +import org.apache.eventmesh.runtime.util.RemotingHelper; +import org.apache.eventmesh.runtime.util.TraceUtils; +import org.apache.eventmesh.runtime.util.Utils; +import org.apache.eventmesh.trace.api.common.EventMeshTraceConstants; + +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; +import io.netty.handler.codec.http.multipart.DiskAttribute; +import io.netty.handler.ssl.SslHandler; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.util.ReferenceCountUtil; +import io.opentelemetry.api.trace.Span; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class AbstractMCPServer extends AbstractRemotingServer{ + private final transient EventMeshHTTPConfiguration eventMeshHttpConfiguration; + + @Getter + @Setter + private EventMeshHttpMetricsManager eventMeshHttpMetricsManager; + + private static final DefaultHttpDataFactory DEFAULT_HTTP_DATA_FACTORY = new DefaultHttpDataFactory(false); + + static { + DiskAttribute.deleteOnExitTemporaryFile = false; + } + + protected final transient AtomicBoolean started = new AtomicBoolean(false); + + @Getter + private final transient boolean useTLS; + + @Getter + @Setter + private Boolean useTrace = false; // Determine whether trace is enabled + + private static final int MAX_CONNECTIONS = 20_000; + + /** + * key: request type + */ + protected final transient Map mcpRequestProcessorTable = + new ConcurrentHashMap<>(64); + + private MCPConnectionHandler mcpConnectionHandler; + private MCPDispatcher mcpDispatcher; + + @Setter + @Getter + private HandlerService handlerService; + + private final transient ThreadPoolExecutor asyncContextCompleteHandler = + ThreadPoolFactory.createThreadPoolExecutor(10, 10, "MCP-asyncContext"); + + @Getter + private final HTTPThreadPoolGroup mcpThreadPoolGroup; + + public AbstractMCPServer(final int port, final boolean useTLS, + final EventMeshHTTPConfiguration eventMeshHttpConfiguration) { + super(); + this.setPort(port); + this.useTLS = useTLS; + this.eventMeshHttpConfiguration = eventMeshHttpConfiguration; + this.mcpThreadPoolGroup = new HTTPThreadPoolGroup(eventMeshHttpConfiguration); + } + + protected void initSharableHandlers() { + mcpConnectionHandler = new MCPConnectionHandler(); + mcpDispatcher = new MCPDispatcher(); + } + + public void init() throws Exception { + super.init("mcp-server"); + initProducerManager(); + mcpThreadPoolGroup.initThreadPool(); + } + + @Override + public CommonConfiguration getConfiguration() { + return eventMeshHttpConfiguration; + } + + @Override + public void start() throws Exception { + + initSharableHandlers(); + + final Thread thread = new Thread(() -> { + final ServerBootstrap bootstrap = new ServerBootstrap(); + try { + bootstrap.group(this.getBossGroup(), this.getIoGroup()) + .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class) + .childHandler(new MCPServerInitializer(useTLS ? SSLContextFactory.getSslContext(eventMeshHttpConfiguration) : null)) + .childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE); + + log.info("MCPServer[port={}] started.", this.getPort()); + + bootstrap.bind(this.getPort()) + .channel() + .closeFuture() + .sync(); + } catch (Exception e) { + log.error("MCPServer start error!", e); + try { + shutdown(); + } catch (Exception ex) { + log.error("MCPServer shutdown error!", ex); + } + System.exit(-1); + } + }, "MCP-server"); + thread.setDaemon(true); + thread.start(); + started.compareAndSet(false, true); + } + + @Override + public void shutdown() throws Exception { + super.shutdown(); + mcpThreadPoolGroup.shutdownThreadPool(); + started.compareAndSet(true, false); + } + + /** + * Registers the processors required by the runtime module + */ + public void registerProcessor(final Integer requestCode, final HttpRequestProcessor processor) { + AssertUtils.notNull(requestCode, "requestCode can't be null"); + AssertUtils.notNull(processor, "processor can't be null"); + this.mcpRequestProcessorTable.putIfAbsent(requestCode.toString(), processor); + } + + /** + * Validate request, return error status. + * + * @param httpRequest + * @return if request is validated return null else return error status + */ + private HttpResponseStatus validateMCPRequest(final HttpRequest httpRequest) { + if (!started.get()) { + return HttpResponseStatus.SERVICE_UNAVAILABLE; + } + + if (!httpRequest.decoderResult().isSuccess()) { + return HttpResponseStatus.BAD_REQUEST; + } + + if (!HttpMethod.POST.equals(httpRequest.method())) { + return HttpResponseStatus.METHOD_NOT_ALLOWED; + } + + // MCP协议检查Content-Type + String contentType = httpRequest.headers().get(HttpHeaderNames.CONTENT_TYPE); + if (StringUtils.isBlank(contentType) || !contentType.contains("application/json")) { + return HttpResponseStatus.BAD_REQUEST; + } + + return null; + } + + public void sendError(final ChannelHandlerContext ctx, final HttpResponseStatus status) { + final FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status); + final HttpHeaders responseHeaders = response.headers(); + responseHeaders.add( + HttpHeaderNames.CONTENT_TYPE, "application/json; charset=UTF-8"); + responseHeaders.add(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); + responseHeaders.add(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + ctx.channel().eventLoop().execute(() -> { + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + }); + } + + public void sendResponse(final ChannelHandlerContext ctx, final DefaultFullHttpResponse response) { + ctx.channel().eventLoop().execute(() -> { + ctx.writeAndFlush(response).addListener((ChannelFutureListener) f -> { + if (!f.isSuccess()) { + log.warn("send response to [{}] fail, will close this channel", RemotingHelper.parseChannelRemoteAddr(f.channel())); + f.channel().close(); + } + }); + }); + } + + /** + * Parse MCP request body to map + * + * @param httpRequest + * @return + */ + private Map parseMCPRequestBody(final HttpRequest httpRequest) throws IOException { + return HttpRequestUtil.parseHttpRequestBody(httpRequest, () -> System.currentTimeMillis(), + (startTime) -> eventMeshHttpMetricsManager.getHttpMetrics().recordDecodeTimeCost(System.currentTimeMillis() - startTime)); + } + + @Sharable + private class MCPDispatcher extends ChannelInboundHandlerAdapter { + + /** + * Is called for each message of type {@link HttpRequest}. + * + * @param ctx the {@link ChannelHandlerContext} which this {@link ChannelInboundHandlerAdapter} belongs to + * @param msg the message to handle + */ + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (!(msg instanceof HttpRequest)) { + return; + } + + HttpRequest httpRequest = (HttpRequest) msg; + + if (Objects.nonNull(handlerService) && handlerService.isProcessorWrapper(httpRequest)) { + handlerService.handler(ctx, httpRequest, asyncContextCompleteHandler); + return; + } + + try { + + Span span = null; + injectMCPRequestHeader(ctx, httpRequest); + + final Map headerMap = Utils.parseHttpHeader(httpRequest); + final HttpResponseStatus errorStatus = validateMCPRequest(httpRequest); + if (errorStatus != null) { + sendError(ctx, errorStatus); + span = TraceUtils.prepareServerSpan(headerMap, + EventMeshTraceConstants.TRACE_UPSTREAM_EVENTMESH_SERVER_SPAN, false); + TraceUtils.finishSpanWithException(span, headerMap, errorStatus.reasonPhrase(), null); + return; + } + eventMeshHttpMetricsManager.getHttpMetrics().recordHTTPRequest(); + + // process MCP request + final HttpCommand requestCommand = new HttpCommand(); + final Map bodyMap = parseMCPRequestBody(httpRequest); + + final String requestCode = HttpMethod.POST.equals(httpRequest.method()) + ? httpRequest.headers().get(ProtocolKey.REQUEST_CODE) + : MapUtils.getString(bodyMap, StringUtils.lowerCase(ProtocolKey.REQUEST_CODE), ""); + + requestCommand.setHttpMethod(httpRequest.method().name()); + requestCommand.setHttpVersion(httpRequest.protocolVersion() == null ? "" + : httpRequest.protocolVersion().protocolName()); + requestCommand.setRequestCode(requestCode); + + HttpCommand responseCommand = null; + + if (StringUtils.isBlank(requestCode) + || !mcpRequestProcessorTable.containsKey(requestCode) + || !RequestCode.contains(Integer.valueOf(requestCode))) { + responseCommand = + requestCommand.createHttpCommandResponse(EventMeshRetCode.EVENTMESH_REQUESTCODE_INVALID); + sendResponse(ctx, responseCommand.httpResponse(HttpResponseStatus.BAD_REQUEST)); + + span = TraceUtils.prepareServerSpan(headerMap, + EventMeshTraceConstants.TRACE_UPSTREAM_EVENTMESH_SERVER_SPAN, false); + TraceUtils.finishSpanWithException(span, headerMap, + EventMeshRetCode.EVENTMESH_REQUESTCODE_INVALID.getErrMsg(), null); + return; + } + + try { + requestCommand.setHeader(Header.buildHeader(requestCode, headerMap)); + requestCommand.setBody(Body.buildBody(requestCode, bodyMap)); + } catch (Exception e) { + responseCommand = requestCommand.createHttpCommandResponse(EventMeshRetCode.EVENTMESH_RUNTIME_ERR); + sendResponse(ctx, responseCommand.httpResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR)); + + span = TraceUtils.prepareServerSpan(headerMap, + EventMeshTraceConstants.TRACE_UPSTREAM_EVENTMESH_SERVER_SPAN, false); + TraceUtils.finishSpanWithException(span, headerMap, + EventMeshRetCode.EVENTMESH_RUNTIME_ERR.getErrMsg(), e); + return; + } + + log.debug("{}", requestCommand); + + final AsyncContext asyncContext = + new AsyncContext<>(requestCommand, responseCommand, asyncContextCompleteHandler); + processMCPCommandRequest(ctx, asyncContext); + + } catch (Exception ex) { + log.error("AbstractMCPServer.MCPDispatcher.channelRead error", ex); + sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR); + } finally { + ReferenceCountUtil.release(httpRequest); + } + } + + /** + * Inject ip and protocol version for MCP. + * + * @param ctx + * @param httpRequest + */ + private void injectMCPRequestHeader(final ChannelHandlerContext ctx, final HttpRequest httpRequest) { + final long startTime = System.currentTimeMillis(); + final HttpHeaders requestHeaders = httpRequest.headers(); + + requestHeaders.set(EventMeshConstants.REQ_C2EVENTMESH_TIMESTAMP, startTime); + if (StringUtils.isBlank(requestHeaders.get(ProtocolKey.VERSION))) { + requestHeaders.set(ProtocolKey.VERSION, ProtocolVersion.V1.getVersion()); + } + + requestHeaders.set(ProtocolKey.ClientInstanceKey.IP.getKey(), + RemotingHelper.parseChannelRemoteAddr(ctx.channel())); + requestHeaders.set(EventMeshConstants.REQ_SEND_EVENTMESH_IP, eventMeshHttpConfiguration.getEventMeshServerIp()); + } + + private void processMCPCommandRequest(final ChannelHandlerContext ctx, final AsyncContext asyncContext) { + final HttpCommand request = asyncContext.getRequest(); + final HttpRequestProcessor choosed = mcpRequestProcessorTable.get(request.getRequestCode()); + Runnable runnable = () -> { + try { + final HttpRequestProcessor processor = choosed; + if (processor.rejectRequest()) { + final HttpCommand responseCommand = + request.createHttpCommandResponse(EventMeshRetCode.EVENTMESH_REJECT_BY_PROCESSOR_ERROR); + asyncContext.onComplete(responseCommand); + + if (asyncContext.isComplete()) { + sendResponse(ctx, responseCommand.httpResponse()); + log.debug("{}", asyncContext.getResponse()); + final Map traceMap = asyncContext.getRequest().getHeader().toMap(); + TraceUtils.finishSpanWithException(TraceUtils.prepareServerSpan(traceMap, + + EventMeshTraceConstants.TRACE_UPSTREAM_EVENTMESH_SERVER_SPAN, + false), + traceMap, + EventMeshRetCode.EVENTMESH_REJECT_BY_PROCESSOR_ERROR.getErrMsg(), null); + } + + return; + } + + processor.processRequest(ctx, asyncContext); + if (!asyncContext.isComplete()) { + return; + } + + log.debug("{}", asyncContext.getResponse()); + eventMeshHttpMetricsManager.getHttpMetrics().recordHTTPReqResTimeCost(System.currentTimeMillis() - request.getReqTime()); + sendResponse(ctx, asyncContext.getResponse().httpResponse()); + + } catch (Exception e) { + log.error("process error", e); + } + }; + + try { + if (Objects.nonNull(choosed.executor())) { + choosed.executor().execute(() -> { + runnable.run(); + }); + } else { + runnable.run(); + } + + } catch (RejectedExecutionException re) { + asyncContext.onComplete(request.createHttpCommandResponse(EventMeshRetCode.OVERLOAD)); + eventMeshHttpMetricsManager.getHttpMetrics().recordHTTPDiscard(); + eventMeshHttpMetricsManager.getHttpMetrics().recordHTTPReqResTimeCost(System.currentTimeMillis() - request.getReqTime()); + try { + sendResponse(ctx, asyncContext.getResponse().httpResponse()); + + final Map traceMap = asyncContext.getRequest().getHeader().toMap(); + + TraceUtils.finishSpanWithException( + TraceUtils.prepareServerSpan(traceMap, + EventMeshTraceConstants.TRACE_UPSTREAM_EVENTMESH_SERVER_SPAN, + false), + traceMap, + EventMeshRetCode.EVENTMESH_RUNTIME_ERR.getErrMsg(), + re); + } catch (Exception e) { + log.error("processEventMeshRequest fail", re); + } + } + } + + @Override + public void channelReadComplete(final ChannelHandlerContext ctx) throws Exception { + super.channelReadComplete(ctx); + ctx.flush(); + } + + @Override + public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) { + if (cause != null) { + log.error("", cause); + } + + if (ctx != null) { + ctx.close(); + } + } + } + + @Sharable + protected class MCPConnectionHandler extends ChannelDuplexHandler { + + public final transient AtomicInteger connections = new AtomicInteger(0); + + @Override + public void channelActive(final ChannelHandlerContext ctx) throws Exception { + if (connections.incrementAndGet() > MAX_CONNECTIONS) { + log.warn("client|mcp|channelActive|remoteAddress={}|msg=too many client({}) connect this MCP server", + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), MAX_CONNECTIONS); + ctx.close(); + return; + } + super.channelActive(ctx); + } + + @Override + public void channelInactive(final ChannelHandlerContext ctx) throws Exception { + connections.decrementAndGet(); + super.channelInactive(ctx); + } + + @Override + public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) { + if (evt instanceof IdleStateEvent) { + final IdleStateEvent event = (IdleStateEvent) evt; + if (event.state().equals(IdleState.ALL_IDLE)) { + final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); + log.info("client|mcp|userEventTriggered|remoteAddress={}|msg={}", remoteAddress, evt.getClass().getName()); + ctx.close(); + } + } + + ctx.fireUserEventTriggered(evt); + } + } + + private class MCPServerInitializer extends ChannelInitializer { + + private final transient SSLContext sslContext; + + public MCPServerInitializer(final SSLContext sslContext) { + this.sslContext = sslContext; + } + + @Override + protected void initChannel(final SocketChannel channel) { + final ChannelPipeline pipeline = channel.pipeline(); + + if (sslContext != null && useTLS) { + final SSLEngine sslEngine = sslContext.createSSLEngine(); + sslEngine.setUseClientMode(false); + pipeline.addFirst(getWorkerGroup(), "ssl", new SslHandler(sslEngine)); + } + + pipeline.addLast(getWorkerGroup(), + new HttpRequestDecoder(), + new HttpResponseEncoder(), + mcpConnectionHandler, + new HttpObjectAggregator(Integer.MAX_VALUE), + mcpDispatcher); + } + } +} diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshMCPServer.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshMCPServer.java new file mode 100644 index 0000000000..f74643ae96 --- /dev/null +++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshMCPServer.java @@ -0,0 +1,197 @@ +package org.apache.eventmesh.runtime.boot; + +import com.google.common.eventbus.EventBus; +import com.google.common.util.concurrent.RateLimiter; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.eventmesh.api.meta.dto.EventMeshRegisterInfo; +import org.apache.eventmesh.api.meta.dto.EventMeshUnRegisterInfo; +import org.apache.eventmesh.common.exception.EventMeshException; +import org.apache.eventmesh.common.protocol.http.common.RequestCode; +import org.apache.eventmesh.common.utils.IPUtils; +import org.apache.eventmesh.metrics.api.MetricsPluginFactory; +import org.apache.eventmesh.metrics.api.MetricsRegistry; +import org.apache.eventmesh.runtime.acl.Acl; +import org.apache.eventmesh.runtime.configuration.EventMeshHTTPConfiguration; +import org.apache.eventmesh.runtime.constants.EventMeshConstants; +import org.apache.eventmesh.runtime.core.consumer.SubscriptionManager; +import org.apache.eventmesh.runtime.core.protocol.http.consumer.ConsumerManager; +import org.apache.eventmesh.runtime.core.protocol.http.processor.*; +import org.apache.eventmesh.runtime.core.protocol.http.push.HTTPClientPool; +import org.apache.eventmesh.runtime.core.protocol.http.retry.HttpRetryer; +import org.apache.eventmesh.runtime.core.protocol.mcp.McpRetryer; +import org.apache.eventmesh.runtime.core.protocol.producer.ProducerManager; +import org.apache.eventmesh.runtime.meta.MetaStorage; +import org.apache.eventmesh.runtime.metrics.http.EventMeshHttpMetricsManager; +import org.assertj.core.util.Lists; + +import java.util.List; +import java.util.Optional; + +import static org.apache.eventmesh.common.Constants.HTTP; + +/** + * Add multiple managers to the underlying server + */ +@Slf4j +@Getter +public class EventMeshMCPServer extends AbstractMCPServer { + private final EventMeshServer eventMeshServer; + private final EventMeshHTTPConfiguration eventMeshHttpConfiguration; + + private final MetaStorage metaStorage; + + private final Acl acl; + private final EventBus eventBus = new EventBus(); + private final transient HTTPClientPool httpClientPool = new HTTPClientPool(10); + private ConsumerManager consumerManager; + private ProducerManager producerManager; + private SubscriptionManager subscriptionManager; + private FilterEngine filterEngine; + private TransformerEngine transformerEngine; + private McpRetryer mcpRetryer; + private transient RateLimiter msgRateLimiter; + private transient RateLimiter batchRateLimiter; + + public EventMeshMCPServer(final EventMeshServer eventMeshServer, final EventMeshHTTPConfiguration eventMeshHttpConfiguration) { + super(eventMeshHttpConfiguration.getHttpServerPort(), + eventMeshHttpConfiguration.isEventMeshServerUseTls(), + eventMeshHttpConfiguration); + this.eventMeshServer = eventMeshServer; + this.eventMeshHttpConfiguration = eventMeshHttpConfiguration; + this.metaStorage = eventMeshServer.getMetaStorage(); + this.acl = eventMeshServer.getAcl(); + } + + public void init() throws Exception { + log.info("==================EventMeshMcpServer Initialing=================="); + super.init(); + + msgRateLimiter = RateLimiter.create(eventMeshHttpConfiguration.getEventMeshHttpMsgReqNumPerSecond()); + batchRateLimiter = RateLimiter.create(eventMeshHttpConfiguration.getEventMeshBatchMsgRequestNumPerSecond()); + + // The MetricsRegistry is singleton, so we can use factory method to get. + final List metricsRegistries = Lists.newArrayList(); + Optional.ofNullable(eventMeshHttpConfiguration.getEventMeshMetricsPluginType()).ifPresent( + metricsPlugins -> metricsPlugins.forEach( + pluginType -> metricsRegistries.add(MetricsPluginFactory.getMetricsRegistry(pluginType)))); + + mcpRetryer = new McpRetryer(this); + + super.setEventMeshHttpMetricsManager(new EventMeshHttpMetricsManager(this, metricsRegistries)); + subscriptionManager = new SubscriptionManager(eventMeshHttpConfiguration.isEventMeshServerMetaStorageEnable(), metaStorage); + + consumerManager = new ConsumerManager(this); + consumerManager.init(); + + producerManager = new ProducerManager(this); + producerManager.init(); + + filterEngine = new FilterEngine(metaStorage, producerManager, consumerManager); + + transformerEngine = new TransformerEngine(metaStorage, producerManager, consumerManager); + + super.setHandlerService(new HandlerService()); + super.getHandlerService().setMetrics(this.getEventMeshHttpMetricsManager()); + + // get the trace-plugin + if (StringUtils.isNotEmpty(eventMeshHttpConfiguration.getEventMeshTracePluginType()) + && eventMeshHttpConfiguration.isEventMeshServerTraceEnable()) { + super.setUseTrace(eventMeshHttpConfiguration.isEventMeshServerTraceEnable()); + } + super.getHandlerService().setHttpTrace(new HTTPTrace(eventMeshHttpConfiguration.isEventMeshServerTraceEnable())); + + registerHTTPRequestProcessor(); + + log.info("==================EventMeshHTTPServer initialized=================="); + } + + @Override + public void start() throws Exception { + super.start(); + this.getEventMeshHttpMetricsManager().start(); + + consumerManager.start(); + producerManager.start(); + mcpRetryer.start(); + // filterEngine depend on metaStorage + if (metaStorage.getStarted().get()) { + filterEngine.start(); + } + + if (eventMeshHttpConfiguration.isEventMeshServerMetaStorageEnable()) { + this.register(); + } + log.info("==================EventMeshHTTPServer started=================="); + } + + @Override + public void shutdown() throws Exception { + + super.shutdown(); + + this.getEventMeshHttpMetricsManager().shutdown(); + + filterEngine.shutdown(); + + transformerEngine.shutdown(); + + consumerManager.shutdown(); + + httpClientPool.shutdown(); + + producerManager.shutdown(); + + mcpRetryer.shutdown(); + + if (eventMeshHttpConfiguration.isEventMeshServerMetaStorageEnable()) { + this.unRegister(); + } + log.info("==================EventMeshHTTPServer shutdown=================="); + } + + /** + * Related to the registry module + */ + public boolean register() { + boolean registerResult = false; + try { + final String endPoints = IPUtils.getLocalAddress() + + EventMeshConstants.IP_PORT_SEPARATOR + eventMeshHttpConfiguration.getHttpServerPort(); + final EventMeshRegisterInfo eventMeshRegisterInfo = new EventMeshRegisterInfo(); + eventMeshRegisterInfo.setEventMeshClusterName(eventMeshHttpConfiguration.getEventMeshCluster()); + eventMeshRegisterInfo.setEventMeshName(eventMeshHttpConfiguration.getEventMeshName() + + "-" + HTTP); + eventMeshRegisterInfo.setEndPoint(endPoints); + eventMeshRegisterInfo.setProtocolType(HTTP); + registerResult = metaStorage.register(eventMeshRegisterInfo); + } catch (Exception e) { + log.error("eventMesh register to registry failed", e); + } + + return registerResult; + } + + /** + * Related to the registry module + */ + private void unRegister() { + final String endPoints = IPUtils.getLocalAddress() + + EventMeshConstants.IP_PORT_SEPARATOR + eventMeshHttpConfiguration.getHttpServerPort(); + final EventMeshUnRegisterInfo eventMeshUnRegisterInfo = new EventMeshUnRegisterInfo(); + eventMeshUnRegisterInfo.setEventMeshClusterName(eventMeshHttpConfiguration.getEventMeshCluster()); + eventMeshUnRegisterInfo.setEventMeshName(eventMeshHttpConfiguration.getEventMeshName()); + eventMeshUnRegisterInfo.setEndPoint(endPoints); + eventMeshUnRegisterInfo.setProtocolType(HTTP); + final boolean registerResult = metaStorage.unRegister(eventMeshUnRegisterInfo); + if (!registerResult) { + throw new EventMeshException("eventMesh fail to unRegister"); + } + } + + private void registerHTTPRequestProcessor() throws Exception { + final BatchSendMessageProcessor batchSendMessageProcessor = new BatchSendMessageProcessor(this); + registerProcessor(RequestCode.MSG_BATCH_SEND.getRequestCode(), batchSendMessageProcessor); + } +} diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshMcpBootstrap.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshMcpBootstrap.java new file mode 100644 index 0000000000..b902815651 --- /dev/null +++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshMcpBootstrap.java @@ -0,0 +1,51 @@ +package org.apache.eventmesh.runtime.boot; + +import lombok.Getter; +import org.apache.eventmesh.common.config.ConfigService; +import org.apache.eventmesh.common.utils.ConfigurationContextUtil; +import org.apache.eventmesh.runtime.configuration.EventMeshHTTPConfiguration; + +import static org.apache.eventmesh.common.Constants.HTTP; + +public class EventMeshMcpBootstrap implements EventMeshBootstrap{ + @Getter + public EventMeshMCPServer eventMeshMCPServer; + + private final EventMeshHTTPConfiguration eventMeshHttpConfiguration; + + private final EventMeshServer eventMeshServer; + + public EventMeshMcpBootstrap(final EventMeshServer eventMeshServer) { + this.eventMeshServer = eventMeshServer; + + ConfigService configService = ConfigService.getInstance(); + this.eventMeshHttpConfiguration = configService.buildConfigInstance(EventMeshHTTPConfiguration.class); + + ConfigurationContextUtil.putIfAbsent(HTTP, eventMeshHttpConfiguration); + } + + @Override + public void init() throws Exception { + // server init + if (eventMeshHttpConfiguration != null) { + eventMeshMCPServer = new EventMeshMCPServer(eventMeshServer, eventMeshHttpConfiguration); + eventMeshMCPServer.init(); + } + } + + @Override + public void start() throws Exception { + // server start + if (eventMeshMCPServer != null) { + eventMeshMCPServer.start(); + } + } + + @Override + public void shutdown() throws Exception { + // server shutdown + if (eventMeshMCPServer != null) { + eventMeshMCPServer.shutdown(); + } + } +} diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/mcp/McpRetryer.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/mcp/McpRetryer.java new file mode 100644 index 0000000000..10a419aade --- /dev/null +++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/mcp/McpRetryer.java @@ -0,0 +1,16 @@ +package org.apache.eventmesh.runtime.core.protocol.mcp; + +import org.apache.eventmesh.retry.api.AbstractRetryer; + +import lombok.extern.slf4j.Slf4j; +import org.apache.eventmesh.runtime.boot.EventMeshMCPServer; + +@Slf4j +public class McpRetryer extends AbstractRetryer { + + private final EventMeshMCPServer eventMeshMCPServer; + + public McpRetryer(EventMeshMCPServer eventMeshMCPServer) { + this.eventMeshMCPServer = eventMeshMCPServer; + } +} From 4c1d227419ddcb5cc68d7232f35ec75027f986a3 Mon Sep 17 00:00:00 2001 From: jerryyummy <461697582@qq.com> Date: Tue, 23 Sep 2025 21:45:44 -0700 Subject: [PATCH 09/36] build the basic mcp server --- .../mcp/sink/data/McpConnectRecord.java | 20 +- .../connector/mcp/source/data/McpRequest.java | 3 - .../mcp/source/protocol/McpConstants.java | 5 - .../runtime/boot/AbstractMCPServer.java | 524 ------------------ .../runtime/boot/EventMeshMCPServer.java | 197 ------- .../runtime/boot/EventMeshMcpBootstrap.java | 51 -- .../runtime/core/protocol/mcp/McpRetryer.java | 16 - settings.gradle | 1 - 8 files changed, 10 insertions(+), 807 deletions(-) delete mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/McpConstants.java delete mode 100644 eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/AbstractMCPServer.java delete mode 100644 eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshMCPServer.java delete mode 100644 eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshMcpBootstrap.java delete mode 100644 eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/mcp/McpRetryer.java diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java index 990b39b4f6..26f9c1e6fb 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java @@ -32,7 +32,7 @@ import lombok.Getter; /** - * a special ConnectRecord for HttpSinkConnector + * a special ConnectRecord for McpSinkConnector */ @Getter @Builder @@ -41,22 +41,22 @@ public class McpConnectRecord implements Serializable { private static final long serialVersionUID = 5271462532332251473L; /** - * The unique identifier for the HttpConnectRecord + * The unique identifier for the McpConnectRecord */ - private final String httpRecordId = UUID.randomUUID().toString(); + private final String mcpRecordId = UUID.randomUUID().toString(); /** - * The time when the HttpConnectRecord was created + * The time when the McpConnectRecord was created */ private LocalDateTime createTime; /** - * The type of the HttpConnectRecord + * The type of the McpConnectRecord */ private String type; /** - * The event id of the HttpConnectRecord + * The event id of the McpConnectRecord */ private String eventId; @@ -66,9 +66,9 @@ public class McpConnectRecord implements Serializable { @Override public String toString() { - return "HttpConnectRecord{" + return "McpConnectRecord{" + "createTime=" + createTime - + ", httpRecordId='" + httpRecordId + + ", mcpRecordId='" + mcpRecordId + ", type='" + type + ", eventId='" + eventId + ", data=" + data @@ -77,10 +77,10 @@ public String toString() { } /** - * Convert ConnectRecord to HttpConnectRecord + * Convert ConnectRecord to McpConnectRecord * * @param record the ConnectRecord to convert - * @return the converted HttpConnectRecord + * @return the converted McpConnectRecord */ public static McpConnectRecord convertConnectRecord(ConnectRecord record, String type) { Map offsetMap = new HashMap<>(); diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java index 8743cf7ac9..6e3cf21a38 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java @@ -40,11 +40,8 @@ public class McpRequest implements Serializable { private String sessionId; - // 元信息,用于携带请求的附加信息,如用户 ID、时间戳、调用方信息、模型版本、语言设置等。服务端可以据此做日志、权限判断、模型路由等操作 private Map metadata; - // 用户输入内容,通常是一个 List,每个元素是一个 Map,表示一轮对话,例如: - //[{"role":"user","content":"你好"}, {"role":"assistant","content":"你好呀"}]。 private Object inputs; private RoutingContext routingContext; diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/McpConstants.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/McpConstants.java deleted file mode 100644 index dee8afd82c..0000000000 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/McpConstants.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.apache.eventmesh.connector.mcp.source.protocol; - - -public class McpConstants { -} diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/AbstractMCPServer.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/AbstractMCPServer.java deleted file mode 100644 index cd3126eb32..0000000000 --- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/AbstractMCPServer.java +++ /dev/null @@ -1,524 +0,0 @@ -package org.apache.eventmesh.runtime.boot; - -import org.apache.eventmesh.common.ThreadPoolFactory; -import org.apache.eventmesh.common.config.CommonConfiguration; -import org.apache.eventmesh.common.protocol.http.HttpCommand; -import org.apache.eventmesh.common.protocol.http.body.Body; -import org.apache.eventmesh.common.protocol.http.common.EventMeshRetCode; -import org.apache.eventmesh.common.protocol.http.common.ProtocolKey; -import org.apache.eventmesh.common.protocol.http.common.ProtocolVersion; -import org.apache.eventmesh.common.protocol.http.common.RequestCode; -import org.apache.eventmesh.common.protocol.http.header.Header; -import org.apache.eventmesh.common.utils.AssertUtils; -import org.apache.eventmesh.runtime.configuration.EventMeshHTTPConfiguration; -import org.apache.eventmesh.runtime.constants.EventMeshConstants; -import org.apache.eventmesh.runtime.core.protocol.http.async.AsyncContext; -import org.apache.eventmesh.runtime.core.protocol.http.processor.HandlerService; -import org.apache.eventmesh.runtime.core.protocol.http.processor.inf.HttpRequestProcessor; -import org.apache.eventmesh.runtime.metrics.http.EventMeshHttpMetricsManager; -import org.apache.eventmesh.runtime.util.HttpRequestUtil; -import org.apache.eventmesh.runtime.util.RemotingHelper; -import org.apache.eventmesh.runtime.util.TraceUtils; -import org.apache.eventmesh.runtime.util.Utils; -import org.apache.eventmesh.trace.api.common.EventMeshTraceConstants; - -import org.apache.commons.collections4.MapUtils; -import org.apache.commons.lang3.StringUtils; - -import java.io.IOException; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; - -import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.ChannelDuplexHandler; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandler.Sharable; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelOption; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.epoll.EpollServerSocketChannel; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.handler.codec.http.DefaultFullHttpResponse; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpHeaderValues; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.HttpObjectAggregator; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpRequestDecoder; -import io.netty.handler.codec.http.HttpResponseEncoder; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; -import io.netty.handler.codec.http.multipart.DiskAttribute; -import io.netty.handler.ssl.SslHandler; -import io.netty.handler.timeout.IdleState; -import io.netty.handler.timeout.IdleStateEvent; -import io.netty.util.ReferenceCountUtil; -import io.opentelemetry.api.trace.Span; - -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public abstract class AbstractMCPServer extends AbstractRemotingServer{ - private final transient EventMeshHTTPConfiguration eventMeshHttpConfiguration; - - @Getter - @Setter - private EventMeshHttpMetricsManager eventMeshHttpMetricsManager; - - private static final DefaultHttpDataFactory DEFAULT_HTTP_DATA_FACTORY = new DefaultHttpDataFactory(false); - - static { - DiskAttribute.deleteOnExitTemporaryFile = false; - } - - protected final transient AtomicBoolean started = new AtomicBoolean(false); - - @Getter - private final transient boolean useTLS; - - @Getter - @Setter - private Boolean useTrace = false; // Determine whether trace is enabled - - private static final int MAX_CONNECTIONS = 20_000; - - /** - * key: request type - */ - protected final transient Map mcpRequestProcessorTable = - new ConcurrentHashMap<>(64); - - private MCPConnectionHandler mcpConnectionHandler; - private MCPDispatcher mcpDispatcher; - - @Setter - @Getter - private HandlerService handlerService; - - private final transient ThreadPoolExecutor asyncContextCompleteHandler = - ThreadPoolFactory.createThreadPoolExecutor(10, 10, "MCP-asyncContext"); - - @Getter - private final HTTPThreadPoolGroup mcpThreadPoolGroup; - - public AbstractMCPServer(final int port, final boolean useTLS, - final EventMeshHTTPConfiguration eventMeshHttpConfiguration) { - super(); - this.setPort(port); - this.useTLS = useTLS; - this.eventMeshHttpConfiguration = eventMeshHttpConfiguration; - this.mcpThreadPoolGroup = new HTTPThreadPoolGroup(eventMeshHttpConfiguration); - } - - protected void initSharableHandlers() { - mcpConnectionHandler = new MCPConnectionHandler(); - mcpDispatcher = new MCPDispatcher(); - } - - public void init() throws Exception { - super.init("mcp-server"); - initProducerManager(); - mcpThreadPoolGroup.initThreadPool(); - } - - @Override - public CommonConfiguration getConfiguration() { - return eventMeshHttpConfiguration; - } - - @Override - public void start() throws Exception { - - initSharableHandlers(); - - final Thread thread = new Thread(() -> { - final ServerBootstrap bootstrap = new ServerBootstrap(); - try { - bootstrap.group(this.getBossGroup(), this.getIoGroup()) - .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class) - .childHandler(new MCPServerInitializer(useTLS ? SSLContextFactory.getSslContext(eventMeshHttpConfiguration) : null)) - .childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE); - - log.info("MCPServer[port={}] started.", this.getPort()); - - bootstrap.bind(this.getPort()) - .channel() - .closeFuture() - .sync(); - } catch (Exception e) { - log.error("MCPServer start error!", e); - try { - shutdown(); - } catch (Exception ex) { - log.error("MCPServer shutdown error!", ex); - } - System.exit(-1); - } - }, "MCP-server"); - thread.setDaemon(true); - thread.start(); - started.compareAndSet(false, true); - } - - @Override - public void shutdown() throws Exception { - super.shutdown(); - mcpThreadPoolGroup.shutdownThreadPool(); - started.compareAndSet(true, false); - } - - /** - * Registers the processors required by the runtime module - */ - public void registerProcessor(final Integer requestCode, final HttpRequestProcessor processor) { - AssertUtils.notNull(requestCode, "requestCode can't be null"); - AssertUtils.notNull(processor, "processor can't be null"); - this.mcpRequestProcessorTable.putIfAbsent(requestCode.toString(), processor); - } - - /** - * Validate request, return error status. - * - * @param httpRequest - * @return if request is validated return null else return error status - */ - private HttpResponseStatus validateMCPRequest(final HttpRequest httpRequest) { - if (!started.get()) { - return HttpResponseStatus.SERVICE_UNAVAILABLE; - } - - if (!httpRequest.decoderResult().isSuccess()) { - return HttpResponseStatus.BAD_REQUEST; - } - - if (!HttpMethod.POST.equals(httpRequest.method())) { - return HttpResponseStatus.METHOD_NOT_ALLOWED; - } - - // MCP协议检查Content-Type - String contentType = httpRequest.headers().get(HttpHeaderNames.CONTENT_TYPE); - if (StringUtils.isBlank(contentType) || !contentType.contains("application/json")) { - return HttpResponseStatus.BAD_REQUEST; - } - - return null; - } - - public void sendError(final ChannelHandlerContext ctx, final HttpResponseStatus status) { - final FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status); - final HttpHeaders responseHeaders = response.headers(); - responseHeaders.add( - HttpHeaderNames.CONTENT_TYPE, "application/json; charset=UTF-8"); - responseHeaders.add(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); - responseHeaders.add(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); - ctx.channel().eventLoop().execute(() -> { - ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); - }); - } - - public void sendResponse(final ChannelHandlerContext ctx, final DefaultFullHttpResponse response) { - ctx.channel().eventLoop().execute(() -> { - ctx.writeAndFlush(response).addListener((ChannelFutureListener) f -> { - if (!f.isSuccess()) { - log.warn("send response to [{}] fail, will close this channel", RemotingHelper.parseChannelRemoteAddr(f.channel())); - f.channel().close(); - } - }); - }); - } - - /** - * Parse MCP request body to map - * - * @param httpRequest - * @return - */ - private Map parseMCPRequestBody(final HttpRequest httpRequest) throws IOException { - return HttpRequestUtil.parseHttpRequestBody(httpRequest, () -> System.currentTimeMillis(), - (startTime) -> eventMeshHttpMetricsManager.getHttpMetrics().recordDecodeTimeCost(System.currentTimeMillis() - startTime)); - } - - @Sharable - private class MCPDispatcher extends ChannelInboundHandlerAdapter { - - /** - * Is called for each message of type {@link HttpRequest}. - * - * @param ctx the {@link ChannelHandlerContext} which this {@link ChannelInboundHandlerAdapter} belongs to - * @param msg the message to handle - */ - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) { - if (!(msg instanceof HttpRequest)) { - return; - } - - HttpRequest httpRequest = (HttpRequest) msg; - - if (Objects.nonNull(handlerService) && handlerService.isProcessorWrapper(httpRequest)) { - handlerService.handler(ctx, httpRequest, asyncContextCompleteHandler); - return; - } - - try { - - Span span = null; - injectMCPRequestHeader(ctx, httpRequest); - - final Map headerMap = Utils.parseHttpHeader(httpRequest); - final HttpResponseStatus errorStatus = validateMCPRequest(httpRequest); - if (errorStatus != null) { - sendError(ctx, errorStatus); - span = TraceUtils.prepareServerSpan(headerMap, - EventMeshTraceConstants.TRACE_UPSTREAM_EVENTMESH_SERVER_SPAN, false); - TraceUtils.finishSpanWithException(span, headerMap, errorStatus.reasonPhrase(), null); - return; - } - eventMeshHttpMetricsManager.getHttpMetrics().recordHTTPRequest(); - - // process MCP request - final HttpCommand requestCommand = new HttpCommand(); - final Map bodyMap = parseMCPRequestBody(httpRequest); - - final String requestCode = HttpMethod.POST.equals(httpRequest.method()) - ? httpRequest.headers().get(ProtocolKey.REQUEST_CODE) - : MapUtils.getString(bodyMap, StringUtils.lowerCase(ProtocolKey.REQUEST_CODE), ""); - - requestCommand.setHttpMethod(httpRequest.method().name()); - requestCommand.setHttpVersion(httpRequest.protocolVersion() == null ? "" - : httpRequest.protocolVersion().protocolName()); - requestCommand.setRequestCode(requestCode); - - HttpCommand responseCommand = null; - - if (StringUtils.isBlank(requestCode) - || !mcpRequestProcessorTable.containsKey(requestCode) - || !RequestCode.contains(Integer.valueOf(requestCode))) { - responseCommand = - requestCommand.createHttpCommandResponse(EventMeshRetCode.EVENTMESH_REQUESTCODE_INVALID); - sendResponse(ctx, responseCommand.httpResponse(HttpResponseStatus.BAD_REQUEST)); - - span = TraceUtils.prepareServerSpan(headerMap, - EventMeshTraceConstants.TRACE_UPSTREAM_EVENTMESH_SERVER_SPAN, false); - TraceUtils.finishSpanWithException(span, headerMap, - EventMeshRetCode.EVENTMESH_REQUESTCODE_INVALID.getErrMsg(), null); - return; - } - - try { - requestCommand.setHeader(Header.buildHeader(requestCode, headerMap)); - requestCommand.setBody(Body.buildBody(requestCode, bodyMap)); - } catch (Exception e) { - responseCommand = requestCommand.createHttpCommandResponse(EventMeshRetCode.EVENTMESH_RUNTIME_ERR); - sendResponse(ctx, responseCommand.httpResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR)); - - span = TraceUtils.prepareServerSpan(headerMap, - EventMeshTraceConstants.TRACE_UPSTREAM_EVENTMESH_SERVER_SPAN, false); - TraceUtils.finishSpanWithException(span, headerMap, - EventMeshRetCode.EVENTMESH_RUNTIME_ERR.getErrMsg(), e); - return; - } - - log.debug("{}", requestCommand); - - final AsyncContext asyncContext = - new AsyncContext<>(requestCommand, responseCommand, asyncContextCompleteHandler); - processMCPCommandRequest(ctx, asyncContext); - - } catch (Exception ex) { - log.error("AbstractMCPServer.MCPDispatcher.channelRead error", ex); - sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR); - } finally { - ReferenceCountUtil.release(httpRequest); - } - } - - /** - * Inject ip and protocol version for MCP. - * - * @param ctx - * @param httpRequest - */ - private void injectMCPRequestHeader(final ChannelHandlerContext ctx, final HttpRequest httpRequest) { - final long startTime = System.currentTimeMillis(); - final HttpHeaders requestHeaders = httpRequest.headers(); - - requestHeaders.set(EventMeshConstants.REQ_C2EVENTMESH_TIMESTAMP, startTime); - if (StringUtils.isBlank(requestHeaders.get(ProtocolKey.VERSION))) { - requestHeaders.set(ProtocolKey.VERSION, ProtocolVersion.V1.getVersion()); - } - - requestHeaders.set(ProtocolKey.ClientInstanceKey.IP.getKey(), - RemotingHelper.parseChannelRemoteAddr(ctx.channel())); - requestHeaders.set(EventMeshConstants.REQ_SEND_EVENTMESH_IP, eventMeshHttpConfiguration.getEventMeshServerIp()); - } - - private void processMCPCommandRequest(final ChannelHandlerContext ctx, final AsyncContext asyncContext) { - final HttpCommand request = asyncContext.getRequest(); - final HttpRequestProcessor choosed = mcpRequestProcessorTable.get(request.getRequestCode()); - Runnable runnable = () -> { - try { - final HttpRequestProcessor processor = choosed; - if (processor.rejectRequest()) { - final HttpCommand responseCommand = - request.createHttpCommandResponse(EventMeshRetCode.EVENTMESH_REJECT_BY_PROCESSOR_ERROR); - asyncContext.onComplete(responseCommand); - - if (asyncContext.isComplete()) { - sendResponse(ctx, responseCommand.httpResponse()); - log.debug("{}", asyncContext.getResponse()); - final Map traceMap = asyncContext.getRequest().getHeader().toMap(); - TraceUtils.finishSpanWithException(TraceUtils.prepareServerSpan(traceMap, - - EventMeshTraceConstants.TRACE_UPSTREAM_EVENTMESH_SERVER_SPAN, - false), - traceMap, - EventMeshRetCode.EVENTMESH_REJECT_BY_PROCESSOR_ERROR.getErrMsg(), null); - } - - return; - } - - processor.processRequest(ctx, asyncContext); - if (!asyncContext.isComplete()) { - return; - } - - log.debug("{}", asyncContext.getResponse()); - eventMeshHttpMetricsManager.getHttpMetrics().recordHTTPReqResTimeCost(System.currentTimeMillis() - request.getReqTime()); - sendResponse(ctx, asyncContext.getResponse().httpResponse()); - - } catch (Exception e) { - log.error("process error", e); - } - }; - - try { - if (Objects.nonNull(choosed.executor())) { - choosed.executor().execute(() -> { - runnable.run(); - }); - } else { - runnable.run(); - } - - } catch (RejectedExecutionException re) { - asyncContext.onComplete(request.createHttpCommandResponse(EventMeshRetCode.OVERLOAD)); - eventMeshHttpMetricsManager.getHttpMetrics().recordHTTPDiscard(); - eventMeshHttpMetricsManager.getHttpMetrics().recordHTTPReqResTimeCost(System.currentTimeMillis() - request.getReqTime()); - try { - sendResponse(ctx, asyncContext.getResponse().httpResponse()); - - final Map traceMap = asyncContext.getRequest().getHeader().toMap(); - - TraceUtils.finishSpanWithException( - TraceUtils.prepareServerSpan(traceMap, - EventMeshTraceConstants.TRACE_UPSTREAM_EVENTMESH_SERVER_SPAN, - false), - traceMap, - EventMeshRetCode.EVENTMESH_RUNTIME_ERR.getErrMsg(), - re); - } catch (Exception e) { - log.error("processEventMeshRequest fail", re); - } - } - } - - @Override - public void channelReadComplete(final ChannelHandlerContext ctx) throws Exception { - super.channelReadComplete(ctx); - ctx.flush(); - } - - @Override - public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) { - if (cause != null) { - log.error("", cause); - } - - if (ctx != null) { - ctx.close(); - } - } - } - - @Sharable - protected class MCPConnectionHandler extends ChannelDuplexHandler { - - public final transient AtomicInteger connections = new AtomicInteger(0); - - @Override - public void channelActive(final ChannelHandlerContext ctx) throws Exception { - if (connections.incrementAndGet() > MAX_CONNECTIONS) { - log.warn("client|mcp|channelActive|remoteAddress={}|msg=too many client({}) connect this MCP server", - RemotingHelper.parseChannelRemoteAddr(ctx.channel()), MAX_CONNECTIONS); - ctx.close(); - return; - } - super.channelActive(ctx); - } - - @Override - public void channelInactive(final ChannelHandlerContext ctx) throws Exception { - connections.decrementAndGet(); - super.channelInactive(ctx); - } - - @Override - public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) { - if (evt instanceof IdleStateEvent) { - final IdleStateEvent event = (IdleStateEvent) evt; - if (event.state().equals(IdleState.ALL_IDLE)) { - final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel()); - log.info("client|mcp|userEventTriggered|remoteAddress={}|msg={}", remoteAddress, evt.getClass().getName()); - ctx.close(); - } - } - - ctx.fireUserEventTriggered(evt); - } - } - - private class MCPServerInitializer extends ChannelInitializer { - - private final transient SSLContext sslContext; - - public MCPServerInitializer(final SSLContext sslContext) { - this.sslContext = sslContext; - } - - @Override - protected void initChannel(final SocketChannel channel) { - final ChannelPipeline pipeline = channel.pipeline(); - - if (sslContext != null && useTLS) { - final SSLEngine sslEngine = sslContext.createSSLEngine(); - sslEngine.setUseClientMode(false); - pipeline.addFirst(getWorkerGroup(), "ssl", new SslHandler(sslEngine)); - } - - pipeline.addLast(getWorkerGroup(), - new HttpRequestDecoder(), - new HttpResponseEncoder(), - mcpConnectionHandler, - new HttpObjectAggregator(Integer.MAX_VALUE), - mcpDispatcher); - } - } -} diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshMCPServer.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshMCPServer.java deleted file mode 100644 index f74643ae96..0000000000 --- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshMCPServer.java +++ /dev/null @@ -1,197 +0,0 @@ -package org.apache.eventmesh.runtime.boot; - -import com.google.common.eventbus.EventBus; -import com.google.common.util.concurrent.RateLimiter; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.apache.eventmesh.api.meta.dto.EventMeshRegisterInfo; -import org.apache.eventmesh.api.meta.dto.EventMeshUnRegisterInfo; -import org.apache.eventmesh.common.exception.EventMeshException; -import org.apache.eventmesh.common.protocol.http.common.RequestCode; -import org.apache.eventmesh.common.utils.IPUtils; -import org.apache.eventmesh.metrics.api.MetricsPluginFactory; -import org.apache.eventmesh.metrics.api.MetricsRegistry; -import org.apache.eventmesh.runtime.acl.Acl; -import org.apache.eventmesh.runtime.configuration.EventMeshHTTPConfiguration; -import org.apache.eventmesh.runtime.constants.EventMeshConstants; -import org.apache.eventmesh.runtime.core.consumer.SubscriptionManager; -import org.apache.eventmesh.runtime.core.protocol.http.consumer.ConsumerManager; -import org.apache.eventmesh.runtime.core.protocol.http.processor.*; -import org.apache.eventmesh.runtime.core.protocol.http.push.HTTPClientPool; -import org.apache.eventmesh.runtime.core.protocol.http.retry.HttpRetryer; -import org.apache.eventmesh.runtime.core.protocol.mcp.McpRetryer; -import org.apache.eventmesh.runtime.core.protocol.producer.ProducerManager; -import org.apache.eventmesh.runtime.meta.MetaStorage; -import org.apache.eventmesh.runtime.metrics.http.EventMeshHttpMetricsManager; -import org.assertj.core.util.Lists; - -import java.util.List; -import java.util.Optional; - -import static org.apache.eventmesh.common.Constants.HTTP; - -/** - * Add multiple managers to the underlying server - */ -@Slf4j -@Getter -public class EventMeshMCPServer extends AbstractMCPServer { - private final EventMeshServer eventMeshServer; - private final EventMeshHTTPConfiguration eventMeshHttpConfiguration; - - private final MetaStorage metaStorage; - - private final Acl acl; - private final EventBus eventBus = new EventBus(); - private final transient HTTPClientPool httpClientPool = new HTTPClientPool(10); - private ConsumerManager consumerManager; - private ProducerManager producerManager; - private SubscriptionManager subscriptionManager; - private FilterEngine filterEngine; - private TransformerEngine transformerEngine; - private McpRetryer mcpRetryer; - private transient RateLimiter msgRateLimiter; - private transient RateLimiter batchRateLimiter; - - public EventMeshMCPServer(final EventMeshServer eventMeshServer, final EventMeshHTTPConfiguration eventMeshHttpConfiguration) { - super(eventMeshHttpConfiguration.getHttpServerPort(), - eventMeshHttpConfiguration.isEventMeshServerUseTls(), - eventMeshHttpConfiguration); - this.eventMeshServer = eventMeshServer; - this.eventMeshHttpConfiguration = eventMeshHttpConfiguration; - this.metaStorage = eventMeshServer.getMetaStorage(); - this.acl = eventMeshServer.getAcl(); - } - - public void init() throws Exception { - log.info("==================EventMeshMcpServer Initialing=================="); - super.init(); - - msgRateLimiter = RateLimiter.create(eventMeshHttpConfiguration.getEventMeshHttpMsgReqNumPerSecond()); - batchRateLimiter = RateLimiter.create(eventMeshHttpConfiguration.getEventMeshBatchMsgRequestNumPerSecond()); - - // The MetricsRegistry is singleton, so we can use factory method to get. - final List metricsRegistries = Lists.newArrayList(); - Optional.ofNullable(eventMeshHttpConfiguration.getEventMeshMetricsPluginType()).ifPresent( - metricsPlugins -> metricsPlugins.forEach( - pluginType -> metricsRegistries.add(MetricsPluginFactory.getMetricsRegistry(pluginType)))); - - mcpRetryer = new McpRetryer(this); - - super.setEventMeshHttpMetricsManager(new EventMeshHttpMetricsManager(this, metricsRegistries)); - subscriptionManager = new SubscriptionManager(eventMeshHttpConfiguration.isEventMeshServerMetaStorageEnable(), metaStorage); - - consumerManager = new ConsumerManager(this); - consumerManager.init(); - - producerManager = new ProducerManager(this); - producerManager.init(); - - filterEngine = new FilterEngine(metaStorage, producerManager, consumerManager); - - transformerEngine = new TransformerEngine(metaStorage, producerManager, consumerManager); - - super.setHandlerService(new HandlerService()); - super.getHandlerService().setMetrics(this.getEventMeshHttpMetricsManager()); - - // get the trace-plugin - if (StringUtils.isNotEmpty(eventMeshHttpConfiguration.getEventMeshTracePluginType()) - && eventMeshHttpConfiguration.isEventMeshServerTraceEnable()) { - super.setUseTrace(eventMeshHttpConfiguration.isEventMeshServerTraceEnable()); - } - super.getHandlerService().setHttpTrace(new HTTPTrace(eventMeshHttpConfiguration.isEventMeshServerTraceEnable())); - - registerHTTPRequestProcessor(); - - log.info("==================EventMeshHTTPServer initialized=================="); - } - - @Override - public void start() throws Exception { - super.start(); - this.getEventMeshHttpMetricsManager().start(); - - consumerManager.start(); - producerManager.start(); - mcpRetryer.start(); - // filterEngine depend on metaStorage - if (metaStorage.getStarted().get()) { - filterEngine.start(); - } - - if (eventMeshHttpConfiguration.isEventMeshServerMetaStorageEnable()) { - this.register(); - } - log.info("==================EventMeshHTTPServer started=================="); - } - - @Override - public void shutdown() throws Exception { - - super.shutdown(); - - this.getEventMeshHttpMetricsManager().shutdown(); - - filterEngine.shutdown(); - - transformerEngine.shutdown(); - - consumerManager.shutdown(); - - httpClientPool.shutdown(); - - producerManager.shutdown(); - - mcpRetryer.shutdown(); - - if (eventMeshHttpConfiguration.isEventMeshServerMetaStorageEnable()) { - this.unRegister(); - } - log.info("==================EventMeshHTTPServer shutdown=================="); - } - - /** - * Related to the registry module - */ - public boolean register() { - boolean registerResult = false; - try { - final String endPoints = IPUtils.getLocalAddress() - + EventMeshConstants.IP_PORT_SEPARATOR + eventMeshHttpConfiguration.getHttpServerPort(); - final EventMeshRegisterInfo eventMeshRegisterInfo = new EventMeshRegisterInfo(); - eventMeshRegisterInfo.setEventMeshClusterName(eventMeshHttpConfiguration.getEventMeshCluster()); - eventMeshRegisterInfo.setEventMeshName(eventMeshHttpConfiguration.getEventMeshName() - + "-" + HTTP); - eventMeshRegisterInfo.setEndPoint(endPoints); - eventMeshRegisterInfo.setProtocolType(HTTP); - registerResult = metaStorage.register(eventMeshRegisterInfo); - } catch (Exception e) { - log.error("eventMesh register to registry failed", e); - } - - return registerResult; - } - - /** - * Related to the registry module - */ - private void unRegister() { - final String endPoints = IPUtils.getLocalAddress() - + EventMeshConstants.IP_PORT_SEPARATOR + eventMeshHttpConfiguration.getHttpServerPort(); - final EventMeshUnRegisterInfo eventMeshUnRegisterInfo = new EventMeshUnRegisterInfo(); - eventMeshUnRegisterInfo.setEventMeshClusterName(eventMeshHttpConfiguration.getEventMeshCluster()); - eventMeshUnRegisterInfo.setEventMeshName(eventMeshHttpConfiguration.getEventMeshName()); - eventMeshUnRegisterInfo.setEndPoint(endPoints); - eventMeshUnRegisterInfo.setProtocolType(HTTP); - final boolean registerResult = metaStorage.unRegister(eventMeshUnRegisterInfo); - if (!registerResult) { - throw new EventMeshException("eventMesh fail to unRegister"); - } - } - - private void registerHTTPRequestProcessor() throws Exception { - final BatchSendMessageProcessor batchSendMessageProcessor = new BatchSendMessageProcessor(this); - registerProcessor(RequestCode.MSG_BATCH_SEND.getRequestCode(), batchSendMessageProcessor); - } -} diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshMcpBootstrap.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshMcpBootstrap.java deleted file mode 100644 index b902815651..0000000000 --- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshMcpBootstrap.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.apache.eventmesh.runtime.boot; - -import lombok.Getter; -import org.apache.eventmesh.common.config.ConfigService; -import org.apache.eventmesh.common.utils.ConfigurationContextUtil; -import org.apache.eventmesh.runtime.configuration.EventMeshHTTPConfiguration; - -import static org.apache.eventmesh.common.Constants.HTTP; - -public class EventMeshMcpBootstrap implements EventMeshBootstrap{ - @Getter - public EventMeshMCPServer eventMeshMCPServer; - - private final EventMeshHTTPConfiguration eventMeshHttpConfiguration; - - private final EventMeshServer eventMeshServer; - - public EventMeshMcpBootstrap(final EventMeshServer eventMeshServer) { - this.eventMeshServer = eventMeshServer; - - ConfigService configService = ConfigService.getInstance(); - this.eventMeshHttpConfiguration = configService.buildConfigInstance(EventMeshHTTPConfiguration.class); - - ConfigurationContextUtil.putIfAbsent(HTTP, eventMeshHttpConfiguration); - } - - @Override - public void init() throws Exception { - // server init - if (eventMeshHttpConfiguration != null) { - eventMeshMCPServer = new EventMeshMCPServer(eventMeshServer, eventMeshHttpConfiguration); - eventMeshMCPServer.init(); - } - } - - @Override - public void start() throws Exception { - // server start - if (eventMeshMCPServer != null) { - eventMeshMCPServer.start(); - } - } - - @Override - public void shutdown() throws Exception { - // server shutdown - if (eventMeshMCPServer != null) { - eventMeshMCPServer.shutdown(); - } - } -} diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/mcp/McpRetryer.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/mcp/McpRetryer.java deleted file mode 100644 index 10a419aade..0000000000 --- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/mcp/McpRetryer.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.apache.eventmesh.runtime.core.protocol.mcp; - -import org.apache.eventmesh.retry.api.AbstractRetryer; - -import lombok.extern.slf4j.Slf4j; -import org.apache.eventmesh.runtime.boot.EventMeshMCPServer; - -@Slf4j -public class McpRetryer extends AbstractRetryer { - - private final EventMeshMCPServer eventMeshMCPServer; - - public McpRetryer(EventMeshMCPServer eventMeshMCPServer) { - this.eventMeshMCPServer = eventMeshMCPServer; - } -} diff --git a/settings.gradle b/settings.gradle index 80da648d5e..327ca7e1a2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -107,7 +107,6 @@ include 'eventmesh-protocol-plugin:eventmesh-protocol-meshmessage' include 'eventmesh-protocol-plugin:eventmesh-protocol-http' include 'eventmesh-protocol-plugin:eventmesh-protocol-grpc' include 'eventmesh-protocol-plugin:eventmesh-protocol-grpcmessage' -include 'eventmesh-protocol-plugin:eventmesh-protocol-mcp' include 'eventmesh-metrics-plugin' include 'eventmesh-metrics-plugin:eventmesh-metrics-api' From 94d9196b3397e79595d04b5f7c53284cacb7bec5 Mon Sep 17 00:00:00 2001 From: jerryyummy <461697582@qq.com> Date: Wed, 24 Sep 2025 12:29:39 -0700 Subject: [PATCH 10/36] build the basic mcp server --- .../connector/mcp/sink/handler/AbstractMcpSinkHandler.java | 2 +- .../connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java index f7b16aa40d..f9f1f0c3a8 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java @@ -71,7 +71,7 @@ private void sendRecordToUrl(ConnectRecord record, Map attribute // add AttemptEvent to the attributes McpAttemptEvent attemptEvent = new McpAttemptEvent(this.sinkConnectorConfig.getRetryConfig().getMaxRetries() + 1); - attributes.put(McpAttemptEvent.PREFIX + mcpConnectRecord.getHttpRecordId(), attemptEvent); + attributes.put(McpAttemptEvent.PREFIX + mcpConnectRecord.getMcpRecordId(), attemptEvent); // deliver the record deliver(url, mcpConnectRecord, attributes, record); diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java index 01cb9c2d08..dee0484fd6 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java @@ -176,7 +176,7 @@ public Future> deliver(URI url, McpConnectRecord mcpConnect */ private void tryCallback(McpConnectRecord mcpConnectRecord, Throwable e, Map attributes, ConnectRecord record) { // get and update the attempt event - McpAttemptEvent attemptEvent = (McpAttemptEvent) attributes.get(McpAttemptEvent.PREFIX + mcpConnectRecord.getHttpRecordId()); + McpAttemptEvent attemptEvent = (McpAttemptEvent) attributes.get(McpAttemptEvent.PREFIX + mcpConnectRecord.getMcpRecordId()); attemptEvent.updateEvent(e); // get and update the multiHttpRequestContext From a5b44db822842feb023d0a6d5a625e3cd2d38159 Mon Sep 17 00:00:00 2001 From: jerryyummy <461697582@qq.com> Date: Wed, 24 Sep 2025 22:03:17 -0700 Subject: [PATCH 11/36] build the basic mcp server --- .../mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java index ae3b9f37d8..e917585752 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java @@ -109,7 +109,7 @@ public Future> deliver(URI url, McpConnectRecord mcpConnect /** - * Cleans up and releases resources used by the Mcp/McpS handler. + * Cleans up and releases resources used by the Mcp handler. */ @Override public void stop() { From 87ceb606bdec67f2d4a39fb0f99a765d845d81ca Mon Sep 17 00:00:00 2001 From: jerryyummy <461697582@qq.com> Date: Thu, 25 Sep 2025 22:07:46 -0700 Subject: [PATCH 12/36] build the basic mcp server --- .../config/connector/mcp/McpSinkConfig.java | 1 + .../config/connector/mcp/McpSourceConfig.java | 4 +- .../connector/mcp/SinkConnectorConfig.java | 1 - .../connector/mcp/SourceConnectorConfig.java | 4 +- .../connector/mcp/config/McpServerConfig.java | 4 +- .../mcp/server/McpConnectServer.java | 17 ++++ .../connector/mcp/sink/McpSinkConnector.java | 86 +++++++++++-------- .../mcp/sink/data/McpConnectRecord.java | 5 +- .../mcp/sink/data/McpExportMetadata.java | 6 +- .../mcp/sink/data/McpExportRecord.java | 4 +- .../mcp/sink/data/McpExportRecordPage.java | 6 +- .../sink/handler/AbstractMcpSinkHandler.java | 17 ++++ .../mcp/sink/handler/McpSinkHandler.java | 7 +- .../handler/impl/CommonMcpSinkHandler.java | 3 +- .../mcp/source/McpSourceConnector.java | 22 +++-- .../connector/mcp/source/data/McpRequest.java | 7 +- .../mcp/source/data/McpResponse.java | 5 +- .../protocol/impl/McpStandardProtocol.java | 14 ++- .../demo/sub/RemoteSubscribeInstance.java | 37 ++++---- 19 files changed, 150 insertions(+), 100 deletions(-) diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpSinkConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpSinkConfig.java index 93ecb78f10..ce645513c5 100644 --- a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpSinkConfig.java +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpSinkConfig.java @@ -22,6 +22,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; + @Data @EqualsAndHashCode(callSuper = true) public class McpSinkConfig extends SinkConfig { diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpSourceConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpSourceConfig.java index 4cf6ae3875..320cc3761f 100644 --- a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpSourceConfig.java +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/McpSourceConfig.java @@ -17,9 +17,11 @@ package org.apache.eventmesh.common.config.connector.mcp; +import org.apache.eventmesh.common.config.connector.SourceConfig; + import lombok.Data; import lombok.EqualsAndHashCode; -import org.apache.eventmesh.common.config.connector.SourceConfig; + @Data @EqualsAndHashCode(callSuper = true) diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SinkConnectorConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SinkConnectorConfig.java index 59df614ae6..54a02fb6b5 100644 --- a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SinkConnectorConfig.java +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SinkConnectorConfig.java @@ -19,7 +19,6 @@ import lombok.Data; -import org.apache.eventmesh.common.config.connector.mcp.McpRetryConfig; @Data public class SinkConnectorConfig { diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SourceConnectorConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SourceConnectorConfig.java index 03b6a48724..39faa2480d 100644 --- a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SourceConnectorConfig.java +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SourceConnectorConfig.java @@ -17,11 +17,11 @@ package org.apache.eventmesh.common.config.connector.mcp; -import lombok.Data; - import java.util.HashMap; import java.util.Map; +import lombok.Data; + @Data public class SourceConnectorConfig { diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/config/McpServerConfig.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/config/McpServerConfig.java index fc9bfd489b..9d325e1c38 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/config/McpServerConfig.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/config/McpServerConfig.java @@ -18,10 +18,10 @@ package org.apache.eventmesh.connector.mcp.config; -import org.apache.eventmesh.common.config.connector.Config; - import lombok.Data; import lombok.EqualsAndHashCode; +import org.apache.eventmesh.common.config.connector.Config; + @Data @EqualsAndHashCode(callSuper = true) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java index 8f6ef0305f..71ea9ae752 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/server/McpConnectServer.java @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.server; import org.apache.eventmesh.connector.mcp.config.McpServerConfig; diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java index 8d97da2a0c..ca711cad04 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java @@ -22,8 +22,8 @@ import lombok.extern.slf4j.Slf4j; import org.apache.eventmesh.common.EventMeshThreadFactory; import org.apache.eventmesh.common.config.connector.Config; -import org.apache.eventmesh.common.config.connector.mcp.SinkConnectorConfig; import org.apache.eventmesh.common.config.connector.mcp.McpSinkConfig; +import org.apache.eventmesh.common.config.connector.mcp.SinkConnectorConfig; import org.apache.eventmesh.connector.mcp.sink.handler.McpSinkHandler; import org.apache.eventmesh.connector.mcp.sink.handler.impl.CommonMcpSinkHandler; import org.apache.eventmesh.connector.mcp.sink.handler.impl.McpSinkHandlerRetryWrapper; @@ -40,6 +40,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; + @Slf4j public class McpSinkConnector implements Sink, ConnectorCreateService { @@ -50,9 +51,7 @@ public class McpSinkConnector implements Sink, ConnectorCreateService { private ThreadPoolExecutor executor; - private final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(10000); - - private final AtomicBoolean isStart = new AtomicBoolean(true); + private final AtomicBoolean isStart = new AtomicBoolean(false); @Override public Class configClass() { @@ -96,30 +95,25 @@ private void doInit() { } else { throw new IllegalArgumentException("Max retries must be greater than or equal to 0."); } + boolean isParallelized = this.mcpSinkConfig.connectorConfig.isParallelized(); int parallelism = isParallelized ? this.mcpSinkConfig.connectorConfig.getParallelism() : 1; - executor = new ThreadPoolExecutor(parallelism, parallelism, 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<>(), new EventMeshThreadFactory("mcp-sink-handler")); + + // Use the executor's built-in queue with a reasonable capacity + executor = new ThreadPoolExecutor( + parallelism, + parallelism, + 0L, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(10000), // Built-in queue with capacity + new EventMeshThreadFactory("mcp-sink-handler") + ); } @Override public void start() throws Exception { this.sinkHandler.start(); - for (int i = 0; i < this.mcpSinkConfig.connectorConfig.getParallelism(); i++) { - executor.execute(() -> { - while (isStart.get()) { - ConnectRecord connectRecord = null; - try { - connectRecord = queue.poll(2, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - if (connectRecord != null) { - sinkHandler.handle(connectRecord); - } - } - }); - } + isStart.set(true); } @Override @@ -140,31 +134,55 @@ public void onException(ConnectRecord record) { @Override public void stop() throws Exception { isStart.set(false); - while (!queue.isEmpty()) { - ConnectRecord record = queue.poll(); - this.sinkHandler.handle(record); - } + + log.info("Stopping mcp sink connector, shutting down executor..."); + executor.shutdown(); + try { - Thread.sleep(50); + // Wait for existing tasks to complete + if (!executor.awaitTermination(30, TimeUnit.SECONDS)) { + log.warn("Executor did not terminate gracefully, forcing shutdown"); + executor.shutdownNow(); + // Wait a bit more for tasks to respond to being cancelled + if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { + log.error("Executor did not terminate after forced shutdown"); + } + } } catch (InterruptedException e) { + log.warn("Interrupted while waiting for executor termination"); + executor.shutdownNow(); Thread.currentThread().interrupt(); } + + log.info("All tasks completed, stopping mcp sink handler"); this.sinkHandler.stop(); - log.info("All tasks completed, start shut down mcp sink connector"); } @Override public void put(List sinkRecords) { + if (!isStart.get()) { + log.warn("Connector is not started, ignoring sink records"); + return; + } + for (ConnectRecord sinkRecord : sinkRecords) { + if (Objects.isNull(sinkRecord)) { + log.warn("ConnectRecord data is null, ignore."); + continue; + } + try { - if (Objects.isNull(sinkRecord)) { - log.warn("ConnectRecord data is null, ignore."); - continue; - } - queue.put(sinkRecord); + // Use executor.submit() instead of custom queue + executor.submit(() -> { + try { + sinkHandler.handle(sinkRecord); + } catch (Exception e) { + log.error("Failed to handle sink record via mcp", e); + } + }); } catch (Exception e) { - log.error("Failed to sink message via mcp. ", e); + log.error("Failed to submit sink record to executor", e); } } } -} +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java index 26f9c1e6fb..ea4e34aec3 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java @@ -17,6 +17,8 @@ package org.apache.eventmesh.connector.mcp.sink.data; +import lombok.Builder; +import lombok.Getter; import org.apache.eventmesh.common.remote.offset.http.HttpRecordOffset; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.KeyValue; @@ -28,9 +30,6 @@ import java.util.Map; import java.util.UUID; -import lombok.Builder; -import lombok.Getter; - /** * a special ConnectRecord for McpSinkConnector */ diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java index 72595b2637..7acdfddf86 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java @@ -17,12 +17,12 @@ package org.apache.eventmesh.connector.mcp.sink.data; -import java.io.Serializable; -import java.time.LocalDateTime; - import lombok.Builder; import lombok.Data; +import java.io.Serializable; +import java.time.LocalDateTime; + /** * Metadata for an MCP export operation. */ diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java index c9a35c193b..8a9d479437 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java @@ -17,11 +17,11 @@ package org.apache.eventmesh.connector.mcp.sink.data; -import java.io.Serializable; - import lombok.AllArgsConstructor; import lombok.Data; +import java.io.Serializable; + /** * Represents an MCP export record containing metadata and data to be exported. */ diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java index 3e0e615523..dfbdccbd4b 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java @@ -17,12 +17,12 @@ package org.apache.eventmesh.connector.mcp.sink.data; -import java.io.Serializable; -import java.util.List; - import lombok.AllArgsConstructor; import lombok.Data; +import java.io.Serializable; +import java.util.List; + /** * Represents a page of MCP export records. */ diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java index f9f1f0c3a8..0a0fea3be5 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.eventmesh.connector.mcp.sink.handler; import lombok.Getter; diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java index c10d96337b..50f0cc318d 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java @@ -17,16 +17,15 @@ package org.apache.eventmesh.connector.mcp.sink.handler; +import io.vertx.core.Future; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.client.HttpResponse; import org.apache.eventmesh.connector.mcp.sink.data.McpConnectRecord; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; import java.net.URI; import java.util.Map; -import io.vertx.core.Future; -import io.vertx.core.buffer.Buffer; -import io.vertx.ext.web.client.HttpResponse; - /** * Interface for handling ConnectRecords via HTTP or HTTPS. Classes implementing this interface are responsible for processing ConnectRecords by * sending them over HTTP or HTTPS, with additional support for handling multiple requests and asynchronous processing. diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java index dee0484fd6..cec2d8591c 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java @@ -30,9 +30,9 @@ import lombok.extern.slf4j.Slf4j; import org.apache.eventmesh.common.config.connector.mcp.SinkConnectorConfig; import org.apache.eventmesh.common.utils.JsonUtils; +import org.apache.eventmesh.connector.http.util.HttpUtils; import org.apache.eventmesh.connector.mcp.sink.data.McpAttemptEvent; import org.apache.eventmesh.connector.mcp.sink.data.McpConnectRecord; -import org.apache.eventmesh.connector.http.util.HttpUtils; import org.apache.eventmesh.connector.mcp.sink.data.MultiMcpRequestContext; import org.apache.eventmesh.connector.mcp.sink.handler.AbstractMcpSinkHandler; import org.apache.eventmesh.openconnect.offsetmgmt.api.callback.SendExceptionContext; @@ -47,6 +47,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; + /** * Common MCP Sink Handler implementation to handle ConnectRecords by sending them over MCP to configured URLs. * diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java index 7ae40504b5..f679ded072 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java @@ -17,6 +17,16 @@ package org.apache.eventmesh.connector.mcp.source; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.ext.web.Route; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.LoggerHandler; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import org.apache.eventmesh.common.config.connector.Config; import org.apache.eventmesh.common.config.connector.mcp.McpSourceConfig; import org.apache.eventmesh.common.exception.EventMeshException; @@ -35,18 +45,6 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.ext.web.Route; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.LoggerHandler; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - @Slf4j public class McpSourceConnector implements Source, ConnectorCreateService { diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java index 6e3cf21a38..a2269e2304 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java @@ -17,15 +17,14 @@ package org.apache.eventmesh.connector.mcp.source.data; -import java.io.Serializable; -import java.util.Map; - import io.vertx.ext.web.RoutingContext; - import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import java.io.Serializable; +import java.util.Map; + /** * Mcp Protocol Request. */ diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java index 32e2910f7a..f79af73f2c 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java @@ -17,15 +17,14 @@ package org.apache.eventmesh.connector.mcp.source.data; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONWriter.Feature; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.time.LocalDateTime; -import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONWriter.Feature; -import java.util.Map; /** * Mcp response. diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java index 74ae98eca1..88619e5c59 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java @@ -17,6 +17,12 @@ package org.apache.eventmesh.connector.mcp.source.protocol.impl; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Route; +import io.vertx.ext.web.handler.BodyHandler; +import lombok.extern.slf4j.Slf4j; import org.apache.eventmesh.common.Constants; import org.apache.eventmesh.common.config.connector.mcp.SourceConnectorConfig; import org.apache.eventmesh.common.utils.JsonUtils; @@ -30,14 +36,6 @@ import java.util.concurrent.BlockingQueue; import java.util.stream.Collectors; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.Route; -import io.vertx.ext.web.handler.BodyHandler; - -import lombok.extern.slf4j.Slf4j; - /** * Common Protocol. This class represents the common webhook protocol. The processing method of this class does not perform any other operations * except storing the request and returning a general response. diff --git a/eventmesh-examples/src/main/java/org/apache/eventmesh/http/demo/sub/RemoteSubscribeInstance.java b/eventmesh-examples/src/main/java/org/apache/eventmesh/http/demo/sub/RemoteSubscribeInstance.java index 1cfba31579..55f79d2248 100644 --- a/eventmesh-examples/src/main/java/org/apache/eventmesh/http/demo/sub/RemoteSubscribeInstance.java +++ b/eventmesh-examples/src/main/java/org/apache/eventmesh/http/demo/sub/RemoteSubscribeInstance.java @@ -17,6 +17,20 @@ package org.apache.eventmesh.http.demo.sub; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; + import org.apache.eventmesh.client.http.EventMeshRetObj; import org.apache.eventmesh.client.http.model.RequestParam; import org.apache.eventmesh.client.http.util.HttpUtils; @@ -32,22 +46,11 @@ import org.apache.eventmesh.common.utils.IPUtils; import org.apache.eventmesh.common.utils.JsonUtils; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - import io.netty.handler.codec.http.HttpMethod; -import org.apache.http.util.EntityUtils; +import lombok.extern.slf4j.Slf4j; + +@Slf4j public class RemoteSubscribeInstance { static final CloseableHttpClient httpClient = HttpClients.createDefault(); @@ -70,7 +73,7 @@ private static void subscribeLocal() throws IOException { body.put("topic", Collections.singletonList(item)); String json = JsonUtils.toJSONString(body); - // 2) 直接用 HttpPost + // 2) use HttpPost HttpPost post = new HttpPost("http://127.0.0.1:10105/eventmesh/subscribe/local"); post.setHeader("Content-Type", "application/json"); post.setHeader("env", "prod"); @@ -84,8 +87,8 @@ private static void subscribeLocal() throws IOException { try (CloseableHttpResponse resp = httpClient.execute(post)) { String respBody = EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8); - System.out.println(resp.getStatusLine()); - System.out.println(respBody); + log.info("respStatusLine:{}", resp.getStatusLine()); + log.info("respBody:{}", respBody); } } From ab5fd0a4581ffb139d4a673e1aa64c7d6a042298 Mon Sep 17 00:00:00 2001 From: jerryyummy <461697582@qq.com> Date: Mon, 29 Sep 2025 00:34:00 -0700 Subject: [PATCH 13/36] build the basic mcp server --- .../mcp/source/McpSourceConnector.java | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java index f679ded072..d1aa4e927e 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java @@ -19,14 +19,23 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; import io.vertx.ext.web.Route; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.client.HttpResponse; +import io.vertx.ext.web.client.WebClient; import io.vertx.ext.web.handler.LoggerHandler; + import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import lombok.var; + import org.apache.eventmesh.common.config.connector.Config; import org.apache.eventmesh.common.config.connector.mcp.McpSourceConfig; import org.apache.eventmesh.common.exception.EventMeshException; @@ -41,7 +50,9 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -66,6 +77,10 @@ public class McpSourceConnector implements Source, ConnectorCreateService sseSessions = new ConcurrentHashMap<>(); + + private WebClient webClient; + @Override public Class configClass() { @@ -104,6 +119,161 @@ private void doInit() { final Vertx vertx = Vertx.vertx(); final Router router = Router.router(vertx); + + final String path = this.sourceConfig.connectorConfig.getPath(); + final int idleMs = this.sourceConfig.connectorConfig.getIdleTimeout(); + final long heartbeatMs = (idleMs > 0) ? Math.max(1000L, idleMs / 2L) : 15000L; + final String rpcPath = path.endsWith("/") ? (path + "rpc") : (path + "/rpc"); + + this.webClient = WebClient.create(vertx); + + router.options(path).handler(ctx -> { + addCors(ctx.response()); + ctx.response().setStatusCode(204).end(); + }); + + router.get(path) + .order(-1) + .handler(LoggerHandler.create()) + .handler(ctx -> { + HttpServerResponse res = ctx.response(); + addCors(res); + res.putHeader("Content-Type", "text/event-stream"); + res.putHeader("Cache-Control", "no-cache"); + res.putHeader("Connection", "keep-alive"); + res.setChunked(true); + + String sid = ctx.request().getHeader("Mcp-Session-Id"); + if (sid == null || sid.isEmpty()) sid = "default"; + sseSessions.put(sid, res); + + writeSseComment(res, "mcp-sse ready"); + + long timerId = vertx.setPeriodic(heartbeatMs, t -> + writeSseComment(res, "keepalive " + System.currentTimeMillis()) + ); + + final String sid0 = sid; + ctx.request().connection() + .closeHandler(v -> { vertx.cancelTimer(timerId); sseSessions.remove(sid0); }) + .exceptionHandler(ex -> { vertx.cancelTimer(timerId); sseSessions.remove(sid0); }); + }); + + router.post(rpcPath).handler(LoggerHandler.create()).handler(ctx -> { + addCors(ctx.response()); + String sid = ctx.request().getHeader("Mcp-Session-Id"); + if (sid == null || sid.isEmpty()) sid = "default"; + HttpServerResponse sse = sseSessions.get(sid); + if (sse == null) { + ctx.response().setStatusCode(409).putHeader("Content-Type","application/json") + .end(new JsonObject().put("error", "no sse session for sid " + sid).encode()); + return; + } + + ctx.request().body().onSuccess(buf -> { + JsonObject root; + try { + root = buf.toJsonObject(); + } catch (Exception e) { + ctx.response().setStatusCode(400).end("{\"error\":\"bad json\"}"); + return; + } + + String jsonrpc = root.getString("jsonrpc", ""); + String method = root.getString("method", ""); + Object idVal = root.getValue("id"); + String idRaw = toRawJson(idVal); + + if (!"2.0".equals(jsonrpc)) { + writeSseMessage(sse, jsonRpcError(idRaw, -32600, "Invalid Request")); + ctx.response().setStatusCode(200).end("{\"ok\":true}"); + return; + } + + if ("initialize".equals(method)) { + String result = new JsonObject() + .put("protocolVersion", "2025-03-26") + .put("capabilities", new JsonObject()) + .encode(); + writeSseMessage(sse, jsonRpcResult(idRaw, result)); + ctx.response().setStatusCode(200).end("{\"ok\":true}"); + return; + } + + if ("tools/list".equals(method)) { + JsonObject inputSchema = new JsonObject() + .put("type", "object") + .put("properties", new JsonObject() + .put("body", new JsonObject().put("type", "object")) + .put("headers", new JsonObject().put("type", "object"))) + .put("required", new JsonArray().add("body")); + + JsonObject tool = new JsonObject() + .put("name", "callConnector") + .put("description", "POST body to " + path) + .put("inputSchema", inputSchema); + + String toolsJson = new JsonObject().put("tools", new JsonArray().add(tool)).encode(); + writeSseMessage(sse, jsonRpcResult(idRaw, toolsJson)); + ctx.response().setStatusCode(200).end("{\"ok\":true}"); + return; + } + + if ("tools/call".equals(method)) { + JsonObject params = root.getJsonObject("params", new JsonObject()); + String toolName = params.getString("name", ""); + JsonObject arguments = params.getJsonObject("arguments", new JsonObject()); + JsonObject bodyObj = arguments.getJsonObject("body", new JsonObject()); + JsonObject headersObj = arguments.getJsonObject("headers", new JsonObject()); + + if (!"callConnector".equals(toolName)) { + writeSseMessage(sse, jsonRpcError(idRaw, -32601, "Unknown tool")); + ctx.response().setStatusCode(200).end("{\"ok\":true}"); + return; + } + + int port = this.sourceConfig.connectorConfig.getPort(); + + var req = webClient.post(port, "127.0.0.1", path); + + for (String key : headersObj.fieldNames()) { + req.putHeader(key, String.valueOf(headersObj.getValue(key))); + } + req.putHeader("Content-Type", "application/json"); + + req.sendBuffer(Buffer.buffer(bodyObj.encode()), ar -> { + int code; + String respText; + boolean isError; + if (ar.succeeded()) { + HttpResponse resp = ar.result(); + code = resp.statusCode(); + respText = resp.bodyAsString(); + isError = code >= 400; + } else { + code = 500; + respText = String.valueOf(ar.cause()); + isError = true; + } + + JsonObject result = new JsonObject() + .put("content", new JsonArray().add( + new JsonObject().put("type","text") + .put("text", "HTTP " + code + "\n" + respText))) + .put("isError", isError); + + writeSseMessage(sse, jsonRpcResult(idRaw, result.encode())); + }); + + ctx.response().setStatusCode(200).end("{\"ok\":true}"); + return; + } + + writeSseMessage(sse, jsonRpcError(idRaw, -32601, "Method not found")); + ctx.response().setStatusCode(200).end("{\"ok\":true}"); + }).onFailure(err -> ctx.response().setStatusCode(400).end()); + }); + route = router.route() .path(this.sourceConfig.connectorConfig.getPath()) .handler(LoggerHandler.create()); @@ -214,4 +384,35 @@ public List poll() { return connectRecords; } + private static void addCors(HttpServerResponse res) { + res.putHeader("Access-Control-Allow-Origin", "*"); + res.putHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS"); + res.putHeader("Access-Control-Allow-Headers", + "Authorization,Content-Type,Accept,Mcp-Protocol-Version,Mcp-Session-Id"); + } + + private static void writeSseComment(HttpServerResponse res, String text) { + res.write(": " + text + "\n\n"); + } + + private static void writeSseMessage(HttpServerResponse res, String jsonLine) { + res.write("event: message\n"); + res.write("data: " + jsonLine + "\n\n"); + } + + private static String toRawJson(Object idVal) { + if (idVal == null) return "null"; + if (idVal instanceof Number || idVal instanceof Boolean) return idVal.toString(); + return "\"" + String.valueOf(idVal).replace("\\","\\\\").replace("\"","\\\"") + "\""; + } + + private static String jsonRpcResult(String idRaw, String resultJson) { + return "{\"jsonrpc\":\"2.0\",\"id\":" + idRaw + ",\"result\":" + resultJson + "}"; + } + + private static String jsonRpcError(String idRaw, int code, String message) { + return "{\"jsonrpc\":\"2.0\",\"id\":" + idRaw + ",\"error\":{\"code\":" + code + ",\"message\":\"" + message + "\"}}"; + } + + } From 64de0ea1245412ef8e761088d33ed630c1f92588 Mon Sep 17 00:00:00 2001 From: jerryyummy <461697582@qq.com> Date: Mon, 13 Oct 2025 22:44:44 -0700 Subject: [PATCH 14/36] build the basic mcp server --- .../mcp/source/McpSourceConnector.java | 638 +++++++++++------- .../mcp/source/McpSourceConstants.java | 467 +++++++++++++ .../connector/mcp/source/McpToolRegistry.java | 137 ++++ .../connector/mcp/source/data/McpRequest.java | 129 +++- .../mcp/source/data/McpResponse.java | 87 ++- .../protocol/impl/McpStandardProtocol.java | 372 ++++++++-- .../src/main/resources/server-config.yml | 2 +- 7 files changed, 1529 insertions(+), 303 deletions(-) create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConstants.java create mode 100644 eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpToolRegistry.java diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java index d1aa4e927e..84f5d0e245 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java @@ -1,3 +1,5 @@ +package org.apache.eventmesh.connector.mcp.source; + /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with @@ -15,31 +17,12 @@ * limitations under the License. */ -package org.apache.eventmesh.connector.mcp.source; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.*; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.Route; -import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.client.HttpResponse; -import io.vertx.ext.web.client.WebClient; -import io.vertx.ext.web.handler.LoggerHandler; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import lombok.var; - import org.apache.eventmesh.common.config.connector.Config; import org.apache.eventmesh.common.config.connector.mcp.McpSourceConfig; import org.apache.eventmesh.common.exception.EventMeshException; -import org.apache.eventmesh.connector.mcp.source.data.McpResponse; import org.apache.eventmesh.connector.mcp.source.protocol.Protocol; import org.apache.eventmesh.connector.mcp.source.protocol.ProtocolFactory; import org.apache.eventmesh.openconnect.api.ConnectorCreateService; @@ -50,12 +33,27 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Route; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import io.vertx.ext.web.handler.LoggerHandler; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * MCP Source Connector for EventMesh + * Implements MCP protocol server allowing AI clients to interact with EventMesh via MCP protocol + */ @Slf4j public class McpSourceConnector implements Source, ConnectorCreateService { @@ -71,17 +69,16 @@ public class McpSourceConnector implements Source, ConnectorCreateService sseSessions = new ConcurrentHashMap<>(); - - private WebClient webClient; - - @Override public Class configClass() { return McpSourceConfig.class; @@ -105,198 +102,401 @@ public void init(ConnectorContext connectorContext) { doInit(); } + /** + * Initialize the connector + */ private void doInit() { - // init queue + log.info("Initializing MCP Source Connector..."); + + // Initialize queue int maxQueueSize = this.sourceConfig.getConnectorConfig().getMaxStorageSize(); this.queue = new LinkedBlockingQueue<>(maxQueueSize); - // init batch size + // Initialize batch size this.batchSize = this.sourceConfig.getConnectorConfig().getBatchSize(); - // init protocol String protocolName = this.sourceConfig.getConnectorConfig().getProtocol(); this.protocol = ProtocolFactory.getInstance(this.sourceConfig.connectorConfig, protocolName); - final Vertx vertx = Vertx.vertx(); + // Initialize tool registry + this.toolRegistry = new McpToolRegistry(); + registerDefaultTools(); + + // Initialize Vertx and router + this.vertx = Vertx.vertx(); final Router router = Router.router(vertx); - final String path = this.sourceConfig.connectorConfig.getPath(); - final int idleMs = this.sourceConfig.connectorConfig.getIdleTimeout(); - final long heartbeatMs = (idleMs > 0) ? Math.max(1000L, idleMs / 2L) : 15000L; - final String rpcPath = path.endsWith("/") ? (path + "rpc") : (path + "/rpc"); + final String basePath = this.sourceConfig.connectorConfig.getPath(); + + log.info("Configuring MCP routes with base path: {}", basePath); - this.webClient = WebClient.create(vertx); + // Configure CORS (must be before all routes) + router.route().handler(ctx -> { + ctx.response() + .putHeader(HEADER_CORS_ALLOW_ORIGIN, CORS_ALLOW_ALL) + .putHeader(HEADER_CORS_ALLOW_METHODS, CORS_ALLOWED_METHODS) + .putHeader(HEADER_CORS_ALLOW_HEADERS, CORS_ALLOWED_HEADERS) + .putHeader(HEADER_CORS_EXPOSE_HEADERS, CORS_EXPOSED_HEADERS); - router.options(path).handler(ctx -> { - addCors(ctx.response()); - ctx.response().setStatusCode(204).end(); + if (HTTP_METHOD_OPTIONS.equals(ctx.request().method().name())) { + ctx.response().setStatusCode(HTTP_STATUS_NO_CONTENT).end(); + } else { + ctx.next(); + } }); - router.get(path) - .order(-1) + // Body handler + router.route().handler(BodyHandler.create()); + + // Main endpoint - handles both JSON-RPC and SSE requests + router.post(basePath) .handler(LoggerHandler.create()) .handler(ctx -> { - HttpServerResponse res = ctx.response(); - addCors(res); - res.putHeader("Content-Type", "text/event-stream"); - res.putHeader("Cache-Control", "no-cache"); - res.putHeader("Connection", "keep-alive"); - res.setChunked(true); - - String sid = ctx.request().getHeader("Mcp-Session-Id"); - if (sid == null || sid.isEmpty()) sid = "default"; - sseSessions.put(sid, res); + String contentType = ctx.request().getHeader(HEADER_CONTENT_TYPE); + String accept = ctx.request().getHeader(HEADER_ACCEPT); - writeSseComment(res, "mcp-sse ready"); + log.info("Request to {} - Content-Type: {}, Accept: {}", + basePath, contentType, accept); - long timerId = vertx.setPeriodic(heartbeatMs, t -> - writeSseComment(res, "keepalive " + System.currentTimeMillis()) - ); + // Determine if it's an SSE request or JSON-RPC request + if (CONTENT_TYPE_SSE.startsWith(accept != null ? accept : "")) { + handleSseRequest(ctx); + } else { + handleJsonRpcRequest(ctx); + } + }); - final String sid0 = sid; - ctx.request().connection() - .closeHandler(v -> { vertx.cancelTimer(timerId); sseSessions.remove(sid0); }) - .exceptionHandler(ex -> { vertx.cancelTimer(timerId); sseSessions.remove(sid0); }); + // GET request for SSE support + router.get(basePath) + .handler(ctx -> { + log.info("GET request to {} - treating as SSE", basePath); + handleSseRequest(ctx); }); - router.post(rpcPath).handler(LoggerHandler.create()).handler(ctx -> { - addCors(ctx.response()); - String sid = ctx.request().getHeader("Mcp-Session-Id"); - if (sid == null || sid.isEmpty()) sid = "default"; - HttpServerResponse sse = sseSessions.get(sid); - if (sse == null) { - ctx.response().setStatusCode(409).putHeader("Content-Type","application/json") - .end(new JsonObject().put("error", "no sse session for sid " + sid).encode()); - return; - } + // Health check endpoint + router.get(basePath + ENDPOINT_HEALTH).handler(ctx -> { + JsonObject health = new JsonObject() + .put(KEY_STATUS, VALUE_STATUS_UP) + .put(KEY_CONNECTOR, DEFAULT_CONNECTOR_NAME) + .put(KEY_TOOLS, toolRegistry.getToolCount()); + ctx.response() + .putHeader(HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON) + .end(health.encode()); + }); - ctx.request().body().onSuccess(buf -> { - JsonObject root; - try { - root = buf.toJsonObject(); - } catch (Exception e) { - ctx.response().setStatusCode(400).end("{\"error\":\"bad json\"}"); - return; - } + this.route = router.route() + .path(this.sourceConfig.connectorConfig.getPath()) + .handler(LoggerHandler.create()); - String jsonrpc = root.getString("jsonrpc", ""); - String method = root.getString("method", ""); - Object idVal = root.getValue("id"); - String idRaw = toRawJson(idVal); - if (!"2.0".equals(jsonrpc)) { - writeSseMessage(sse, jsonRpcError(idRaw, -32600, "Invalid Request")); - ctx.response().setStatusCode(200).end("{\"ok\":true}"); - return; - } + // set protocol handler + this.protocol.setHandler(route, queue); - if ("initialize".equals(method)) { - String result = new JsonObject() - .put("protocolVersion", "2025-03-26") - .put("capabilities", new JsonObject()) - .encode(); - writeSseMessage(sse, jsonRpcResult(idRaw, result)); - ctx.response().setStatusCode(200).end("{\"ok\":true}"); - return; - } + // Create server + this.server = vertx.createHttpServer(new HttpServerOptions() + .setPort(this.sourceConfig.connectorConfig.getPort()) + .setHandle100ContinueAutomatically(true) + .setIdleTimeout(DEFAULT_IDLE_TIMEOUT_MS) + .setIdleTimeoutUnit(TimeUnit.MILLISECONDS)) + .requestHandler(router); + + log.info("MCP Source Connector initialized on http://127.0.0.1:{}{}", + this.sourceConfig.connectorConfig.getPort(), basePath); + } - if ("tools/list".equals(method)) { - JsonObject inputSchema = new JsonObject() - .put("type", "object") - .put("properties", new JsonObject() - .put("body", new JsonObject().put("type", "object")) - .put("headers", new JsonObject().put("type", "object"))) - .put("required", new JsonArray().add("body")); - - JsonObject tool = new JsonObject() - .put("name", "callConnector") - .put("description", "POST body to " + path) - .put("inputSchema", inputSchema); - - String toolsJson = new JsonObject().put("tools", new JsonArray().add(tool)).encode(); - writeSseMessage(sse, jsonRpcResult(idRaw, toolsJson)); - ctx.response().setStatusCode(200).end("{\"ok\":true}"); - return; + /** + * Register default MCP tools + */ + private void registerDefaultTools() { + // Echo tool + toolRegistry.registerTool( + TOOL_ECHO, + TOOL_DESC_ECHO, + createEchoSchema(), + args -> { + String message = args.getString(PARAM_MESSAGE, DEFAULT_NO_MESSAGE); + return createTextContent("Echo: " + message); } + ); + + // EventMesh message sending tool (example) + toolRegistry.registerTool( + TOOL_SEND_MESSAGE, + TOOL_DESC_SEND_MESSAGE, + createSendMessageSchema(), + args -> { + String topic = args.getString(PARAM_TOPIC); + String message = args.getString(PARAM_MESSAGE); + + // TODO: Implement actual EventMesh message sending logic + // Messages can be queued and processed by poll() method + + return createTextContent( + String.format("Message sent to topic '%s': %s", topic, message) + ); + } + ); - if ("tools/call".equals(method)) { - JsonObject params = root.getJsonObject("params", new JsonObject()); - String toolName = params.getString("name", ""); - JsonObject arguments = params.getJsonObject("arguments", new JsonObject()); - JsonObject bodyObj = arguments.getJsonObject("body", new JsonObject()); - JsonObject headersObj = arguments.getJsonObject("headers", new JsonObject()); - - if (!"callConnector".equals(toolName)) { - writeSseMessage(sse, jsonRpcError(idRaw, -32601, "Unknown tool")); - ctx.response().setStatusCode(200).end("{\"ok\":true}"); - return; - } + log.info("Registered {} MCP tools", toolRegistry.getToolCount()); + } - int port = this.sourceConfig.connectorConfig.getPort(); + /** + * Handle JSON-RPC request (HTTP mode) + * @param ctx Routing context + */ + private void handleJsonRpcRequest(RoutingContext ctx) { + String body = ctx.body().asString(); + log.info("<<< JSON-RPC Request: {}", body); - var req = webClient.post(port, "127.0.0.1", path); + try { + JsonObject request = new JsonObject(body); + JsonObject response = handleMcpRequest(request); - for (String key : headersObj.fieldNames()) { - req.putHeader(key, String.valueOf(headersObj.getValue(key))); - } - req.putHeader("Content-Type", "application/json"); - - req.sendBuffer(Buffer.buffer(bodyObj.encode()), ar -> { - int code; - String respText; - boolean isError; - if (ar.succeeded()) { - HttpResponse resp = ar.result(); - code = resp.statusCode(); - respText = resp.bodyAsString(); - isError = code >= 400; - } else { - code = 500; - respText = String.valueOf(ar.cause()); - isError = true; - } - - JsonObject result = new JsonObject() - .put("content", new JsonArray().add( - new JsonObject().put("type","text") - .put("text", "HTTP " + code + "\n" + respText))) - .put("isError", isError); - - writeSseMessage(sse, jsonRpcResult(idRaw, result.encode())); - }); - - ctx.response().setStatusCode(200).end("{\"ok\":true}"); - return; - } + if (response != null) { + log.info(">>> JSON-RPC Response: {}", response.encode()); + ctx.response() + .putHeader(HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON) + .end(response.encode()); + } else { + // Notification messages don't need response + ctx.response().setStatusCode(HTTP_STATUS_NO_CONTENT).end(); + } + + } catch (Exception e) { + log.error("Error handling JSON-RPC request", e); + JsonObject error = createErrorResponse(null, ERROR_INTERNAL, + "Internal error: " + e.getMessage()); + ctx.response() + .putHeader(HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON) + .setStatusCode(HTTP_STATUS_INTERNAL_ERROR) + .end(error.encode()); + } + } - writeSseMessage(sse, jsonRpcError(idRaw, -32601, "Method not found")); - ctx.response().setStatusCode(200).end("{\"ok\":true}"); - }).onFailure(err -> ctx.response().setStatusCode(400).end()); + /** + * Handle SSE request (Server-Sent Events mode) + * @param ctx Routing context + */ + private void handleSseRequest(RoutingContext ctx) { + log.info("Establishing SSE connection..."); + + ctx.response() + .putHeader(HEADER_CONTENT_TYPE, CONTENT_TYPE_SSE) + .putHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE) + .putHeader(HEADER_CONNECTION, CONNECTION_KEEP_ALIVE) + .putHeader(HEADER_X_ACCEL_BUFFERING, X_ACCEL_BUFFERING_NO) + .setChunked(true); + + // Send connection established event + ctx.response().write(SSE_EVENT_OPEN); + ctx.response().write(SSE_DATA_OPEN); + + log.info("SSE connection established"); + + // Heartbeat (optional) + long timerId = vertx.setPeriodic(DEFAULT_HEARTBEAT_INTERVAL_MS, id -> { + if (!ctx.response().closed()) { + ctx.response().write(SSE_HEARTBEAT); + } else { + vertx.cancelTimer(id); + } }); - route = router.route() - .path(this.sourceConfig.connectorConfig.getPath()) - .handler(LoggerHandler.create()); + ctx.request().connection().closeHandler(v -> { + log.info("SSE connection closed"); + vertx.cancelTimer(timerId); + }); + } - // set protocol handler - this.protocol.setHandler(route, queue); + /** + * Handle MCP JSON-RPC request + * @param request JSON-RPC request object + * @return JSON-RPC response object, or null for notifications + */ + private JsonObject handleMcpRequest(JsonObject request) { + String method = request.getString(KEY_METHOD, ""); + Object id = request.getValue(KEY_ID); + JsonObject params = request.getJsonObject(KEY_PARAMS); + + log.info("Handling MCP method: {}", method); + + switch (method) { + case METHOD_INITIALIZE: + return handleInitialize(id, params); + case METHOD_NOTIFICATIONS_INITIALIZED: + log.info("Client sent initialized notification"); + return null; // Notifications don't need response + case METHOD_TOOLS_LIST: + return handleToolsList(id); + case METHOD_TOOLS_CALL: + return handleToolsCall(id, params); + case METHOD_PING: + return createSuccessResponse(id, new JsonObject()); + default: + return createErrorResponse(id, ERROR_METHOD_NOT_FOUND, + "Method not found: " + method); + } + } - // create server - this.server = vertx.createHttpServer(new HttpServerOptions() - .setPort(this.sourceConfig.connectorConfig.getPort()) - .setMaxFormAttributeSize(this.sourceConfig.connectorConfig.getMaxFormAttributeSize()) - .setIdleTimeout(this.sourceConfig.connectorConfig.getIdleTimeout()) - .setIdleTimeoutUnit(TimeUnit.MILLISECONDS)).requestHandler(router); + /** + * Handle initialize method + * @param id Request ID + * @param params Request parameters + * @return Initialize response + */ + private JsonObject handleInitialize(Object id, JsonObject params) { + String clientVersion = params != null ? + params.getString(KEY_PROTOCOL_VERSION, DEFAULT_PROTOCOL_VERSION) + : DEFAULT_PROTOCOL_VERSION; + + log.info("Initialize - Protocol version: {}", clientVersion); + + JsonObject result = new JsonObject() + .put(KEY_PROTOCOL_VERSION, clientVersion) + .put(KEY_SERVER_INFO, new JsonObject() + .put(KEY_NAME, DEFAULT_SERVER_NAME) + .put(KEY_VERSION, DEFAULT_SERVER_VERSION)) + .put(KEY_CAPABILITIES, new JsonObject() + .put(KEY_TOOLS, new JsonObject())); + + return createSuccessResponse(id, result); + } + + /** + * Handle tools/list method + * @param id Request ID + * @return Tools list response + */ + private JsonObject handleToolsList(Object id) { + JsonArray tools = toolRegistry.getToolsArray(); + JsonObject result = new JsonObject().put(KEY_TOOLS, tools); + return createSuccessResponse(id, result); + } + + /** + * Handle tools/call method + * @param id Request ID + * @param params Tool call parameters + * @return Tool execution result + */ + private JsonObject handleToolsCall(Object id, JsonObject params) { + if (params == null) { + return createErrorResponse(id, ERROR_INVALID_PARAMS, "Invalid params"); + } + + String toolName = params.getString(KEY_NAME); + JsonObject arguments = params.getJsonObject("arguments", new JsonObject()); + + log.info("Calling tool: {} with arguments: {}", toolName, arguments); + + try { + JsonObject content = toolRegistry.executeTool(toolName, arguments); + JsonObject result = new JsonObject() + .put(KEY_CONTENT, new JsonArray().add(content)); + + return createSuccessResponse(id, result); + + } catch (IllegalArgumentException e) { + return createErrorResponse(id, ERROR_INVALID_PARAMS, e.getMessage()); + } catch (Exception e) { + log.error("Tool execution error", e); + return createErrorResponse(id, ERROR_INTERNAL, + "Tool execution failed: " + e.getMessage()); + } + } + + // ========== JSON-RPC Response Builders ========== + + /** + * Create a success response + * @param id Request ID + * @param result Result object + * @return JSON-RPC success response + */ + private JsonObject createSuccessResponse(Object id, JsonObject result) { + return new JsonObject() + .put(KEY_JSONRPC, VALUE_JSONRPC_VERSION) + .put(KEY_ID, id) + .put(KEY_RESULT, result); + } + + /** + * Create an error response + * @param id Request ID + * @param code Error code + * @param message Error message + * @return JSON-RPC error response + */ + private JsonObject createErrorResponse(Object id, int code, String message) { + return new JsonObject() + .put(KEY_JSONRPC, VALUE_JSONRPC_VERSION) + .put(KEY_ID, id) + .put(KEY_ERROR, new JsonObject() + .put(KEY_ERROR_CODE, code) + .put(KEY_ERROR_MESSAGE, message)); + } + + // ========== Schema Creation Helpers ========== + + /** + * Create JSON schema for echo tool + * @return Echo tool input schema + */ + private JsonObject createEchoSchema() { + return new JsonObject() + .put(KEY_TYPE, VALUE_TYPE_OBJECT) + .put(KEY_PROPERTIES, new JsonObject() + .put(PARAM_MESSAGE, new JsonObject() + .put(KEY_TYPE, VALUE_TYPE_STRING) + .put(KEY_DESCRIPTION, PARAM_DESC_MESSAGE))) + .put(KEY_REQUIRED, new JsonArray().add(PARAM_MESSAGE)); } + /** + * Create JSON schema for send message tool + * @return Send message tool input schema + */ + private JsonObject createSendMessageSchema() { + return new JsonObject() + .put(KEY_TYPE, VALUE_TYPE_OBJECT) + .put(KEY_PROPERTIES, new JsonObject() + .put(PARAM_TOPIC, new JsonObject() + .put(KEY_TYPE, VALUE_TYPE_STRING) + .put(KEY_DESCRIPTION, PARAM_DESC_TOPIC)) + .put(PARAM_MESSAGE, new JsonObject() + .put(KEY_TYPE, VALUE_TYPE_STRING) + .put(KEY_DESCRIPTION, PARAM_DESC_MESSAGE_CONTENT))) + .put(KEY_REQUIRED, new JsonArray().add(PARAM_TOPIC).add(PARAM_MESSAGE)); + } + + /** + * Create text content object + * @param text Text content + * @return MCP text content object + */ + private JsonObject createTextContent(String text) { + return new JsonObject() + .put(KEY_TYPE, VALUE_TYPE_TEXT) + .put(KEY_TEXT, text); + } + + // ========== Source Interface Implementation ========== + @Override public void start() { this.server.listen(res -> { if (res.succeeded()) { this.started = true; - log.info("McpSourceConnector started on port: {}", this.sourceConfig.getConnectorConfig().getPort()); + log.info("McpSourceConnector started on port: {}", + this.sourceConfig.getConnectorConfig().getPort()); + log.info("MCP endpoints available at:"); + log.info(" - POST {} (JSON-RPC)", this.sourceConfig.connectorConfig.getPath()); + log.info(" - GET {} (SSE)", this.sourceConfig.connectorConfig.getPath()); + log.info(" - GET {}{} (Health check)", + this.sourceConfig.connectorConfig.getPath(), ENDPOINT_HEALTH); } else { - log.error("McpSourceConnector failed to start on port: {}", this.sourceConfig.getConnectorConfig().getPort()); + log.error("McpSourceConnector failed to start on port: {}", + this.sourceConfig.getConnectorConfig().getPort()); throw new EventMeshException("failed to start Vertx server", res.cause()); } }); @@ -306,15 +506,7 @@ public void start() { public void commit(ConnectRecord record) { if (sourceConfig.getConnectorConfig().isDataConsistencyEnabled()) { log.debug("McpSourceConnector commit record: {}", record.getRecordId()); - RoutingContext routingContext = (RoutingContext) record.getExtensionObj("routingContext"); - if (routingContext != null) { - routingContext.response() - .putHeader("content-type", "application/json") - .setStatusCode(HttpResponseStatus.OK.code()) - .end(McpResponse.success().toJsonStr()); - } else { - log.error("Failed to commit the record, routingContext is null, recordId: {}", record.getRecordId()); - } + // MCP protocol processing doesn't require additional commit logic } } @@ -325,42 +517,38 @@ public String name() { @Override public void onException(ConnectRecord record) { - if (this.route != null) { - this.route.failureHandler(ctx -> { - log.error("Failed to handle the request, recordId {}. ", record.getRecordId(), ctx.failure()); - // Return Bad Response - ctx.response() - .setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()) - .end("{\"status\":\"failed\",\"recordId\":\"" + record.getRecordId() + "\"}"); - }); - } + log.error("Exception occurred for record: {}", record.getRecordId()); + // MCP errors are already handled via JSON-RPC error responses } @Override public void stop() { if (this.server != null) { this.server.close(res -> { - if (res.succeeded()) { - this.destroyed = true; - log.info("McpSourceConnector stopped on port: {}", this.sourceConfig.getConnectorConfig().getPort()); - } else { - log.error("McpSourceConnector failed to stop on port: {}", this.sourceConfig.getConnectorConfig().getPort()); - throw new EventMeshException("failed to stop Vertx server", res.cause()); - } - } - ); + if (res.succeeded()) { + this.destroyed = true; + log.info("McpSourceConnector stopped on port: {}", + this.sourceConfig.getConnectorConfig().getPort()); + } else { + log.error("McpSourceConnector failed to stop on port: {}", + this.sourceConfig.getConnectorConfig().getPort()); + throw new EventMeshException("failed to stop Vertx server", res.cause()); + } + }); } else { log.warn("McpSourceConnector server is null, ignore."); } + + if (this.vertx != null) { + this.vertx.close(); + } } @Override public List poll() { long startTime = System.currentTimeMillis(); - long maxPollWaitTime = 5000; - long remainingTime = maxPollWaitTime; + long remainingTime = MAX_POLL_WAIT_TIME_MS; - // poll from queue List connectRecords = new ArrayList<>(batchSize); for (int i = 0; i < batchSize; i++) { try { @@ -368,51 +556,19 @@ public List poll() { if (obj == null) { break; } - // convert to ConnectRecord + + // Convert MCP tool calls to ConnectRecord ConnectRecord connectRecord = protocol.convertToConnectRecord(obj); connectRecords.add(connectRecord); - // calculate elapsed time and update remaining time for next poll long elapsedTime = System.currentTimeMillis() - startTime; - remainingTime = maxPollWaitTime > elapsedTime ? maxPollWaitTime - elapsedTime : 0; + remainingTime = MAX_POLL_WAIT_TIME_MS > elapsedTime + ? MAX_POLL_WAIT_TIME_MS - elapsedTime : 0; } catch (Exception e) { log.error("Failed to poll from queue.", e); throw new RuntimeException(e); } - } return connectRecords; } - - private static void addCors(HttpServerResponse res) { - res.putHeader("Access-Control-Allow-Origin", "*"); - res.putHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS"); - res.putHeader("Access-Control-Allow-Headers", - "Authorization,Content-Type,Accept,Mcp-Protocol-Version,Mcp-Session-Id"); - } - - private static void writeSseComment(HttpServerResponse res, String text) { - res.write(": " + text + "\n\n"); - } - - private static void writeSseMessage(HttpServerResponse res, String jsonLine) { - res.write("event: message\n"); - res.write("data: " + jsonLine + "\n\n"); - } - - private static String toRawJson(Object idVal) { - if (idVal == null) return "null"; - if (idVal instanceof Number || idVal instanceof Boolean) return idVal.toString(); - return "\"" + String.valueOf(idVal).replace("\\","\\\\").replace("\"","\\\"") + "\""; - } - - private static String jsonRpcResult(String idRaw, String resultJson) { - return "{\"jsonrpc\":\"2.0\",\"id\":" + idRaw + ",\"result\":" + resultJson + "}"; - } - - private static String jsonRpcError(String idRaw, int code, String message) { - return "{\"jsonrpc\":\"2.0\",\"id\":" + idRaw + ",\"error\":{\"code\":" + code + ",\"message\":\"" + message + "\"}}"; - } - - -} +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConstants.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConstants.java new file mode 100644 index 0000000000..c60c57276e --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConstants.java @@ -0,0 +1,467 @@ +package org.apache.eventmesh.connector.mcp.source; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/** + * Constants for MCP Source Connector + */ +public final class McpSourceConstants { + + private McpSourceConstants() { + // Utility class, no instantiation + } + + // ========== Server Configuration ========== + + /** + * Default connector name + */ + public static final String DEFAULT_CONNECTOR_NAME = "mcp-source"; + + /** + * Default server name for MCP protocol + */ + public static final String DEFAULT_SERVER_NAME = "eventmesh-mcp-connector"; + + /** + * Default server version + */ + public static final String DEFAULT_SERVER_VERSION = "1.0.0"; + + /** + * Default MCP protocol version + */ + public static final String DEFAULT_PROTOCOL_VERSION = "2024-11-05"; + + /** + * Default idle timeout in milliseconds (60 seconds) + */ + public static final int DEFAULT_IDLE_TIMEOUT_MS = 60000; + + /** + * Default heartbeat interval in milliseconds (30 seconds) + */ + public static final long DEFAULT_HEARTBEAT_INTERVAL_MS = 30000; + + /** + * Maximum poll wait time in milliseconds (5 seconds) + */ + public static final long MAX_POLL_WAIT_TIME_MS = 5000; + + // ========== HTTP Headers ========== + + /** + * Content-Type header name + */ + public static final String HEADER_CONTENT_TYPE = "Content-Type"; + + /** + * Accept header name + */ + public static final String HEADER_ACCEPT = "Accept"; + + /** + * Access-Control-Allow-Origin header + */ + public static final String HEADER_CORS_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + + /** + * Access-Control-Allow-Methods header + */ + public static final String HEADER_CORS_ALLOW_METHODS = "Access-Control-Allow-Methods"; + + /** + * Access-Control-Allow-Headers header + */ + public static final String HEADER_CORS_ALLOW_HEADERS = "Access-Control-Allow-Headers"; + + /** + * Access-Control-Expose-Headers header + */ + public static final String HEADER_CORS_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; + + /** + * Cache-Control header + */ + public static final String HEADER_CACHE_CONTROL = "Cache-Control"; + + /** + * Connection header + */ + public static final String HEADER_CONNECTION = "Connection"; + + /** + * X-Accel-Buffering header + */ + public static final String HEADER_X_ACCEL_BUFFERING = "X-Accel-Buffering"; + + // ========== CORS Values ========== + + /** + * CORS allow all origins + */ + public static final String CORS_ALLOW_ALL = "*"; + + /** + * CORS allowed methods + */ + public static final String CORS_ALLOWED_METHODS = "GET, POST, OPTIONS"; + + /** + * CORS allowed headers + */ + public static final String CORS_ALLOWED_HEADERS = "Content-Type, Authorization, Accept"; + + /** + * CORS exposed headers + */ + public static final String CORS_EXPOSED_HEADERS = "Content-Type"; + + // ========== Content Types ========== + + /** + * JSON content type with UTF-8 charset + */ + public static final String CONTENT_TYPE_JSON = "application/json; charset=utf-8"; + + /** + * Server-Sent Events content type + */ + public static final String CONTENT_TYPE_SSE = "text/event-stream; charset=utf-8"; + + /** + * Plain JSON content type (for matching) + */ + public static final String CONTENT_TYPE_JSON_PLAIN = "application/json"; + + // ========== HTTP Status Codes ========== + + /** + * HTTP 204 No Content + */ + public static final int HTTP_STATUS_NO_CONTENT = 204; + + /** + * HTTP 500 Internal Server Error + */ + public static final int HTTP_STATUS_INTERNAL_ERROR = 500; + + // ========== JSON-RPC Methods ========== + + /** + * Initialize method + */ + public static final String METHOD_INITIALIZE = "initialize"; + + /** + * Notifications initialized method + */ + public static final String METHOD_NOTIFICATIONS_INITIALIZED = "notifications/initialized"; + + /** + * Tools list method + */ + public static final String METHOD_TOOLS_LIST = "tools/list"; + + /** + * Tools call method + */ + public static final String METHOD_TOOLS_CALL = "tools/call"; + + /** + * Ping method + */ + public static final String METHOD_PING = "ping"; + + // ========== JSON-RPC Error Codes ========== + + /** + * Invalid params error code + */ + public static final int ERROR_INVALID_PARAMS = -32602; + + /** + * Method not found error code + */ + public static final int ERROR_METHOD_NOT_FOUND = -32601; + + /** + * Internal error code + */ + public static final int ERROR_INTERNAL = -32603; + + // ========== JSON Keys ========== + + /** + * JSON-RPC version key + */ + public static final String KEY_JSONRPC = "jsonrpc"; + + /** + * JSON-RPC version value + */ + public static final String VALUE_JSONRPC_VERSION = "2.0"; + + /** + * Method key + */ + public static final String KEY_METHOD = "method"; + + /** + * ID key + */ + public static final String KEY_ID = "id"; + + /** + * Params key + */ + public static final String KEY_PARAMS = "params"; + + /** + * Result key + */ + public static final String KEY_RESULT = "result"; + + /** + * Error key + */ + public static final String KEY_ERROR = "error"; + + /** + * Error code key + */ + public static final String KEY_ERROR_CODE = "code"; + + /** + * Error message key + */ + public static final String KEY_ERROR_MESSAGE = "message"; + + /** + * Protocol version key + */ + public static final String KEY_PROTOCOL_VERSION = "protocolVersion"; + + /** + * Server info key + */ + public static final String KEY_SERVER_INFO = "serverInfo"; + + /** + * Capabilities key + */ + public static final String KEY_CAPABILITIES = "capabilities"; + + /** + * Tools key + */ + public static final String KEY_TOOLS = "tools"; + + /** + * Name key + */ + public static final String KEY_NAME = "name"; + + /** + * Version key + */ + public static final String KEY_VERSION = "version"; + + /** + * Content key + */ + public static final String KEY_CONTENT = "content"; + + /** + * Type key + */ + public static final String KEY_TYPE = "type"; + + /** + * Text key + */ + public static final String KEY_TEXT = "text"; + + /** + * Description key + */ + public static final String KEY_DESCRIPTION = "description"; + + /** + * Input schema key + */ + public static final String KEY_INPUT_SCHEMA = "inputSchema"; + + /** + * Properties key + */ + public static final String KEY_PROPERTIES = "properties"; + + /** + * Required key + */ + public static final String KEY_REQUIRED = "required"; + + /** + * Status key + */ + public static final String KEY_STATUS = "status"; + + /** + * Connector key + */ + public static final String KEY_CONNECTOR = "connector"; + + // ========== JSON Values ========== + + /** + * Object type value + */ + public static final String VALUE_TYPE_OBJECT = "object"; + + /** + * String type value + */ + public static final String VALUE_TYPE_STRING = "string"; + + /** + * Text type value + */ + public static final String VALUE_TYPE_TEXT = "text"; + + /** + * Status UP value + */ + public static final String VALUE_STATUS_UP = "UP"; + + // ========== HTTP Methods ========== + + /** + * OPTIONS HTTP method + */ + public static final String HTTP_METHOD_OPTIONS = "OPTIONS"; + + // ========== SSE Events ========== + + /** + * SSE event: open + */ + public static final String SSE_EVENT_OPEN = "event: open\n"; + + /** + * SSE data for open event + */ + public static final String SSE_DATA_OPEN = "data: {\"type\":\"open\"}\n\n"; + + /** + * SSE heartbeat comment + */ + public static final String SSE_HEARTBEAT = ": heartbeat\n\n"; + + /** + * Cache control: no-cache + */ + public static final String CACHE_CONTROL_NO_CACHE = "no-cache"; + + /** + * Connection: keep-alive + */ + public static final String CONNECTION_KEEP_ALIVE = "keep-alive"; + + /** + * X-Accel-Buffering: no + */ + public static final String X_ACCEL_BUFFERING_NO = "no"; + + // ========== Tool Names ========== + + /** + * Echo tool name + */ + public static final String TOOL_ECHO = "echo"; + + /** + * Get current timestamp tool name + */ + public static final String TOOL_GET_TIMESTAMP = "getCurrentTimestamp"; + + /** + * Send EventMesh message tool name + */ + public static final String TOOL_SEND_MESSAGE = "sendEventMeshMessage"; + + // ========== Tool Descriptions ========== + + /** + * Echo tool description + */ + public static final String TOOL_DESC_ECHO = "Echo back the input message"; + + /** + * Get timestamp tool description + */ + public static final String TOOL_DESC_TIMESTAMP = "Get current Unix timestamp in milliseconds"; + + /** + * Send message tool description + */ + public static final String TOOL_DESC_SEND_MESSAGE = "Send a message to EventMesh"; + + // ========== Tool Parameter Names ========== + + /** + * Message parameter name + */ + public static final String PARAM_MESSAGE = "message"; + + /** + * Topic parameter name + */ + public static final String PARAM_TOPIC = "topic"; + + // ========== Tool Parameter Descriptions ========== + + /** + * Message parameter description + */ + public static final String PARAM_DESC_MESSAGE = "Message to echo"; + + /** + * Topic parameter description + */ + public static final String PARAM_DESC_TOPIC = "EventMesh topic"; + + /** + * Message content parameter description + */ + public static final String PARAM_DESC_MESSAGE_CONTENT = "Message content"; + + // ========== Default Values ========== + + /** + * Default message when no message provided + */ + public static final String DEFAULT_NO_MESSAGE = "No message"; + + // ========== Endpoint Paths ========== + + /** + * Health check endpoint suffix + */ + public static final String ENDPOINT_HEALTH = "/health"; +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpToolRegistry.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpToolRegistry.java new file mode 100644 index 0000000000..0d127874a5 --- /dev/null +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpToolRegistry.java @@ -0,0 +1,137 @@ +package org.apache.eventmesh.connector.mcp.source; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * MCP Tool Registry + * Manages all available MCP tools + */ +@Slf4j +public class McpToolRegistry { + + private final Map tools = new ConcurrentHashMap<>(); + + /** + * Register an MCP tool + * @param name Tool name + * @param description Tool description + * @param inputSchema JSON schema for tool input parameters + * @param executor Tool execution logic + */ + public void registerTool(String name, String description, + JsonObject inputSchema, ToolExecutor executor) { + McpTool tool = new McpTool(name, description, inputSchema, executor); + tools.put(name, tool); + log.info("Registered MCP tool: {}", name); + } + + /** + * Execute a specified tool + * @param name Tool name + * @param arguments Tool arguments as JSON object + * @return Tool execution result as MCP content object + * @throws IllegalArgumentException if tool not found + * @throws RuntimeException if tool execution fails + */ + public JsonObject executeTool(String name, JsonObject arguments) { + McpTool tool = tools.get(name); + if (tool == null) { + throw new IllegalArgumentException("Unknown tool: " + name); + } + + try { + return tool.executor.execute(arguments); + } catch (Exception e) { + log.error("Tool execution failed: {}", name, e); + throw new RuntimeException("Tool execution failed: " + e.getMessage(), e); + } + } + + /** + * Get all tools as a JSON array + * @return JSON array containing all registered tools with their metadata + */ + public JsonArray getToolsArray() { + JsonArray array = new JsonArray(); + for (McpTool tool : tools.values()) { + JsonObject toolObj = new JsonObject() + .put("name", tool.name) + .put("description", tool.description) + .put("inputSchema", tool.inputSchema); + array.add(toolObj); + } + return array; + } + + /** + * Get the number of registered tools + * @return Total count of registered tools + */ + public int getToolCount() { + return tools.size(); + } + + /** + * Check if a tool exists + * @param name Tool name to check + * @return true if tool is registered, false otherwise + */ + public boolean hasTool(String name) { + return tools.containsKey(name); + } + + /** + * Tool Executor Interface + * Functional interface for defining tool execution logic + */ + @FunctionalInterface + public interface ToolExecutor { + /** + * Execute tool logic + * @param arguments Tool input arguments as JSON object + * @return MCP content object (must contain 'type' and 'text' fields) + * @throws Exception if execution fails + */ + JsonObject execute(JsonObject arguments) throws Exception; + } + + /** + * MCP Tool Definition + * Internal class representing a registered MCP tool + */ + private static class McpTool { + final String name; + final String description; + final JsonObject inputSchema; + final ToolExecutor executor; + + McpTool(String name, String description, JsonObject inputSchema, ToolExecutor executor) { + this.name = name; + this.description = description; + this.inputSchema = inputSchema; + this.executor = executor; + } + } +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java index a2269e2304..61ee2c650e 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java @@ -17,32 +17,153 @@ package org.apache.eventmesh.connector.mcp.source.data; +import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; +import java.util.HashMap; import java.util.Map; /** - * Mcp Protocol Request. + * MCP Protocol Request + * Represents a request in the MCP (Model Context Protocol) format */ @Data +@Builder @NoArgsConstructor @AllArgsConstructor public class McpRequest implements Serializable { private static final long serialVersionUID = -483500600756490500L; + /** + * Protocol name (should be "MCP") + */ private String protocolName; + /** + * Session ID for tracking the request + */ private String sessionId; - private Map metadata; + /** + * JSON-RPC request ID + */ + private Object requestId; + /** + * MCP method name (e.g., "initialize", "tools/call") + */ + private String method; + + /** + * Tool name (for tools/call method) + */ + private String toolName; + + /** + * Tool arguments (for tools/call method) + */ + private JsonObject arguments; + + /** + * Tool execution result + */ + private JsonObject result; + + /** + * Request timestamp + */ + private long timestamp; + + /** + * Whether the tool execution succeeded + */ + private boolean success; + + /** + * Error message if execution failed + */ + private String errorMessage; + + /** + * Additional metadata + */ + @Builder.Default + private Map metadata = new HashMap<>(); + + /** + * Request input data + */ private Object inputs; - private RoutingContext routingContext; + /** + * Vert.x routing context for HTTP response handling + */ + private transient RoutingContext routingContext; + + /** + * Add a metadata entry + * + * @param key Metadata key + * @param value Metadata value + */ + public void addMetadata(String key, String value) { + if (this.metadata == null) { + this.metadata = new HashMap<>(); + } + this.metadata.put(key, value); + } + + /** + * Get metadata value by key + * + * @param key Metadata key + * @return Metadata value, or null if not found + */ + public String getMetadata(String key) { + return this.metadata != null ? this.metadata.get(key) : null; + } + + /** + * Check if this is a tool call request + * + * @return true if this is a tools/call request + */ + public boolean isToolCall() { + return "tools/call".equals(this.method); + } + + /** + * Check if this is an initialize request + * + * @return true if this is an initialize request + */ + public boolean isInitialize() { + return "initialize".equals(this.method); + } -} + /** + * Convert to a simple map representation + * + * @return Map containing key request information + */ + public Map toMap() { + Map map = new HashMap<>(); + map.put("protocolName", protocolName); + map.put("sessionId", sessionId); + map.put("requestId", requestId); + map.put("method", method); + map.put("toolName", toolName); + map.put("timestamp", timestamp); + map.put("success", success); + if (errorMessage != null) { + map.put("errorMessage", errorMessage); + } + return map; + } +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java index f79af73f2c..b439c6cd50 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java @@ -27,44 +27,109 @@ import java.time.LocalDateTime; /** - * Mcp response. + * MCP Response + * Represents a response message for MCP protocol operations */ @Data @NoArgsConstructor @AllArgsConstructor public class McpResponse implements Serializable { + private static final long serialVersionUID = 8616938575207104455L; + /** + * Response status: "success", "error", etc. + */ + private String status; + + /** + * Response message + */ private String msg; + /** + * Response timestamp + */ private LocalDateTime handleTime; /** - * Convert to json string. + * Additional error code for error responses + */ + private Integer errorCode; + + /** + * Additional data payload + */ + private Object data; + + /** + * Convert to JSON string * - * @return json string + * @return JSON string representation */ public String toJsonStr() { return JSON.toJSONString(this, Feature.WriteMapNullValue); } /** - * Create a success response. + * Create a success response * - * @return response + * @return Success response */ public static McpResponse success() { - return base("success"); + return new McpResponse("success", "Operation completed successfully", + LocalDateTime.now(), null, null); } + /** + * Create a success response with message + * + * @param msg Success message + * @return Success response + */ + public static McpResponse success(String msg) { + return new McpResponse("success", msg, LocalDateTime.now(), null, null); + } + + /** + * Create a success response with data + * + * @param msg Success message + * @param data Response data + * @return Success response with data + */ + public static McpResponse success(String msg, Object data) { + return new McpResponse("success", msg, LocalDateTime.now(), null, data); + } + + /** + * Create an error response + * + * @param msg Error message + * @return Error response + */ + public static McpResponse error(String msg) { + return new McpResponse("error", msg, LocalDateTime.now(), null, null); + } + + /** + * Create an error response with error code + * + * @param msg Error message + * @param errorCode Error code + * @return Error response with code + */ + public static McpResponse error(String msg, Integer errorCode) { + return new McpResponse("error", msg, LocalDateTime.now(), errorCode, null); + } /** - * Create a base response. + * Create a base response * - * @param msg message - * @return response + * @param msg Message + * @return Base response */ public static McpResponse base(String msg) { - return new McpResponse(msg, LocalDateTime.now()); + return new McpResponse("info", msg, LocalDateTime.now(), null, null); } -} +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java index 88619e5c59..9d721fa14a 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java @@ -25,109 +25,389 @@ import lombok.extern.slf4j.Slf4j; import org.apache.eventmesh.common.Constants; import org.apache.eventmesh.common.config.connector.mcp.SourceConnectorConfig; -import org.apache.eventmesh.common.utils.JsonUtils; +import org.apache.eventmesh.common.remote.offset.RecordOffset; +import org.apache.eventmesh.common.remote.offset.RecordPartition; import org.apache.eventmesh.connector.mcp.source.data.McpRequest; import org.apache.eventmesh.connector.mcp.source.data.McpResponse; import org.apache.eventmesh.connector.mcp.source.protocol.Protocol; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; +import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.HashMap; import java.util.Map; +import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.stream.Collectors; /** - * Common Protocol. This class represents the common webhook protocol. The processing method of this class does not perform any other operations - * except storing the request and returning a general response. + * MCP Standard Protocol Implementation + * Handles MCP (Model Context Protocol) requests and converts them to EventMesh ConnectRecords */ @Slf4j public class McpStandardProtocol implements Protocol { + /** + * Protocol name constant + */ public static final String PROTOCOL_NAME = "MCP"; + // Extension keys + private static final String EXTENSION_PROTOCOL = "protocol"; + private static final String EXTENSION_SESSION_ID = "sessionId"; + private static final String EXTENSION_TOOL_NAME = "toolName"; + private static final String EXTENSION_METHOD = "method"; + private static final String EXTENSION_REQUEST_ID = "requestId"; + private static final String EXTENSION_SUCCESS = "success"; + private static final String EXTENSION_ERROR_MESSAGE = "errorMessage"; + private static final String EXTENSION_ROUTING_CONTEXT = "routingContext"; + private static final String EXTENSION_IS_BASE64 = "isBase64"; + private static final String METADATA_EXTENSION_KEY = "extension"; + private SourceConnectorConfig sourceConnectorConfig; /** * Initialize the protocol * - * @param sourceConnectorConfig source connector config + * @param sourceConnectorConfig Source connector configuration */ @Override public void initialize(SourceConnectorConfig sourceConnectorConfig) { this.sourceConnectorConfig = sourceConnectorConfig; + log.info("Initialized MCP Standard Protocol"); } /** * Set the handler for the route + * This method is called when using the protocol in a generic HTTP connector context * - * @param route route - * @param queue queue info + * @param route Vert.x route to configure + * @param queue Queue for storing requests */ @Override public void setHandler(Route route, BlockingQueue queue) { route.method(HttpMethod.POST) .handler(BodyHandler.create()) .handler(ctx -> { - // Get the payload - Object payload = ctx.body().asString(Constants.DEFAULT_CHARSET.toString()); - payload = JsonUtils.parseObject(JsonUtils.toJSONString(payload), String.class); - - // Create and store the webhook request - Map headerMap = ctx.request().headers().entries().stream() - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - McpRequest mcpRequest = new McpRequest(PROTOCOL_NAME, ctx.request().absoluteURI(), headerMap, payload, ctx); - if (!queue.offer(mcpRequest)) { - throw new IllegalStateException("Failed to store the request."); - } + try { + // Parse the request body + String bodyString = ctx.body().asString(Constants.DEFAULT_CHARSET.toString()); + log.debug("Received MCP request: {}", bodyString); + + // Try to parse as JSON + JsonObject requestJson; + try { + requestJson = new JsonObject(bodyString); + } catch (Exception e) { + log.error("Failed to parse request as JSON: {}", bodyString, e); + ctx.response() + .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) + .putHeader("Content-Type", "application/json") + .end(McpResponse.error("Invalid JSON format").toJsonStr()); + return; + } + + // Extract JSON-RPC fields + String method = requestJson.getString("method", ""); + Object requestId = requestJson.getValue("id"); + JsonObject params = requestJson.getJsonObject("params"); + + // Extract headers as metadata + Map metadata = ctx.request().headers().entries().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (v1, v2) -> v1 // Keep first value if duplicate keys + )); + + // Generate session ID if not present + String sessionId = ctx.request().getHeader("Mcp-Session-Id"); + if (sessionId == null || sessionId.isEmpty()) { + sessionId = generateSessionId(); + } + + // Create MCP request based on method type + McpRequest mcpRequest = createMcpRequest( + method, + requestId, + params, + sessionId, + metadata, + bodyString, + ctx + ); + + // Queue the request + if (!queue.offer(mcpRequest)) { + log.error("Failed to queue MCP request: queue is full"); + ctx.response() + .setStatusCode(HttpResponseStatus.SERVICE_UNAVAILABLE.code()) + .putHeader("Content-Type", "application/json") + .end(McpResponse.error("Service temporarily unavailable").toJsonStr()); + return; + } + + // If data consistency is not enabled, return immediate response + if (!sourceConnectorConfig.isDataConsistencyEnabled()) { + ctx.response() + .setStatusCode(HttpResponseStatus.OK.code()) + .putHeader("Content-Type", "application/json") + .end(McpResponse.success().toJsonStr()); + } + // Otherwise, response will be sent after processing (via commit) - if (!sourceConnectorConfig.isDataConsistencyEnabled()) { - // Return 200 OK + } catch (Exception e) { + log.error("Error handling MCP request", e); ctx.response() - .setStatusCode(HttpResponseStatus.OK.code()) - .end(McpResponse.success().toJsonStr()); + .setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()) + .putHeader("Content-Type", "application/json") + .end(McpResponse.error("Internal server error: " + e.getMessage()).toJsonStr()); } - }) .failureHandler(ctx -> { - log.error("Failed to handle the request. ", ctx.failure()); + log.error("Failed to handle MCP request", ctx.failure()); - // Return Bad Response + // Return error response ctx.response() - .setStatusCode(ctx.statusCode()) - .end(McpResponse.base(ctx.failure().getMessage()).toJsonStr()); + .setStatusCode(ctx.statusCode() > 0 ? ctx.statusCode() : 500) + .putHeader("Content-Type", "application/json") + .end(McpResponse.error(ctx.failure().getMessage()).toJsonStr()); }); + } + + /** + * Create MCP request from parsed JSON-RPC data + * + * @param method JSON-RPC method name + * @param requestId JSON-RPC request ID + * @param params JSON-RPC params + * @param sessionId Session identifier + * @param metadata Request metadata (headers) + * @param rawBody Raw request body + * @param ctx Routing context + * @return Constructed McpRequest + */ + private McpRequest createMcpRequest( + String method, + Object requestId, + JsonObject params, + String sessionId, + Map metadata, + String rawBody, + io.vertx.ext.web.RoutingContext ctx) { + + McpRequest.McpRequestBuilder builder = McpRequest.builder() + .protocolName(PROTOCOL_NAME) + .sessionId(sessionId) + .requestId(requestId) + .method(method) + .timestamp(System.currentTimeMillis()) + .inputs(rawBody) + .routingContext(ctx); + + // Set metadata + if (metadata != null && !metadata.isEmpty()) { + builder.metadata(metadata); + } else { + builder.metadata(new HashMap<>()); + } + + // Handle different method types + if ("tools/call".equals(method) && params != null) { + // Tool call request + String toolName = params.getString("name"); + JsonObject arguments = params.getJsonObject("arguments", new JsonObject()); + builder.toolName(toolName) + .arguments(arguments) + .success(false); // Will be set to true after execution + + log.debug("Created tool call request: tool={}, id={}", toolName, requestId); + + } else if ("initialize".equals(method)) { + // Initialize request + log.debug("Created initialize request: id={}", requestId); + builder.success(true); + + } else { + // Other methods + log.debug("Created generic MCP request: method={}, id={}", method, requestId); + builder.success(true); + } + + return builder.build(); } /** - * Convert the message to a connect record + * Convert MCP request to ConnectRecord + * Simple and direct conversion following the existing pattern * - * @param message message - * @return connect record + * @param message MCP request message + * @return ConnectRecord representation */ @Override public ConnectRecord convertToConnectRecord(Object message) { + // Validate input + if (message == null) { + throw new IllegalArgumentException("Message cannot be null"); + } + + if (!(message instanceof McpRequest)) { + throw new IllegalArgumentException( + String.format("Expected McpRequest but got %s", message.getClass().getName()) + ); + } + McpRequest request = (McpRequest) message; - ConnectRecord connectRecord = new ConnectRecord(null, null, System.currentTimeMillis(), request.getInputs()); - connectRecord.addExtension("protocol", PROTOCOL_NAME); - connectRecord.addExtension("session_id", request.getSessionId()); - request.getMetadata().forEach((k, v) -> { - if (k.equalsIgnoreCase("extension")) { - JsonObject extension = new JsonObject(v); - extension.forEach(e -> connectRecord.addExtension(e.getKey(), e.getValue())); - } - }); - // check data - if (connectRecord.getExtensionObj("isBase64") != null) { - if (Boolean.parseBoolean(connectRecord.getExtensionObj("isBase64").toString())) { - byte[] data = Base64.getDecoder().decode(connectRecord.getData().toString()); - connectRecord.setData(data); - } + // Get timestamp + long timestamp = request.getTimestamp() > 0 + ? request.getTimestamp() + : System.currentTimeMillis(); + + // Get data (priority: result > arguments > inputs) + Object data = extractData(request); + + // Create ConnectRecord + ConnectRecord connectRecord = new ConnectRecord(null, null, timestamp, data); + + // Add protocol extension + connectRecord.addExtension(EXTENSION_PROTOCOL, PROTOCOL_NAME); + + // Add session ID + if (request.getSessionId() != null) { + connectRecord.addExtension(EXTENSION_SESSION_ID, request.getSessionId()); + } + + // Add request ID + if (request.getRequestId() != null) { + connectRecord.addExtension(EXTENSION_REQUEST_ID, String.valueOf(request.getRequestId())); + } + + // Add method + if (request.getMethod() != null) { + connectRecord.addExtension(EXTENSION_METHOD, request.getMethod()); + } + + // Add tool name (for tool calls) + if (request.getToolName() != null) { + connectRecord.addExtension(EXTENSION_TOOL_NAME, request.getToolName()); } + + // Add success status + connectRecord.addExtension(EXTENSION_SUCCESS, String.valueOf(request.isSuccess())); + + // Add error message if failed + if (!request.isSuccess() && request.getErrorMessage() != null) { + connectRecord.addExtension(EXTENSION_ERROR_MESSAGE, request.getErrorMessage()); + } + + // Add metadata from request + if (request.getMetadata() != null) { + request.getMetadata().forEach((key, value) -> { + try { + // Handle nested extension objects + if (METADATA_EXTENSION_KEY.equalsIgnoreCase(key)) { + JsonObject extension = new JsonObject(value); + extension.forEach(entry -> + connectRecord.addExtension(entry.getKey(), entry.getValue()) + ); + } else { + connectRecord.addExtension(key, value); + } + } catch (Exception e) { + log.warn("Failed to add metadata: key={}, error={}", key, e.getMessage()); + connectRecord.addExtension(key, value); + } + }); + } + + // Handle Base64 decoding if needed + handleBase64Decoding(connectRecord); + + // Add routing context for response handling if (request.getRoutingContext() != null) { - connectRecord.addExtension("routingContext", request.getRoutingContext()); + connectRecord.addExtension(EXTENSION_ROUTING_CONTEXT, request.getRoutingContext()); } + + log.debug("Converted MCP request to ConnectRecord: method={}, tool={}, id={}", + request.getMethod(), request.getToolName(), request.getRequestId()); + return connectRecord; } -} + + /** + * Extract data from MCP request + * Priority: result > arguments > inputs + */ + private Object extractData(McpRequest request) { + // 1. If tool execution succeeded, use result + if (request.isSuccess() && request.getResult() != null) { + return request.getResult().encode(); + } + + // 2. If tool call, use arguments + if (request.getArguments() != null) { + return request.getArguments().encode(); + } + + // 3. Use inputs (raw request body) + if (request.getInputs() != null) { + return request.getInputs(); + } + + // 4. Fallback: create minimal info + return String.format("{\"method\":\"%s\",\"timestamp\":%d}", + request.getMethod(), request.getTimestamp()); + } + + /** + * Handle Base64 decoding if isBase64 flag is set + */ + private void handleBase64Decoding(ConnectRecord connectRecord) { + Object isBase64Obj = connectRecord.getExtensionObj(EXTENSION_IS_BASE64); + + if (isBase64Obj == null) { + return; + } + + // Parse boolean value + boolean isBase64; + if (isBase64Obj instanceof Boolean) { + isBase64 = (Boolean) isBase64Obj; + } else { + isBase64 = Boolean.parseBoolean(String.valueOf(isBase64Obj)); + } + + // Decode if needed + if (isBase64 && connectRecord.getData() != null) { + try { + String dataStr = connectRecord.getData().toString(); + byte[] decodedData = Base64.getDecoder().decode(dataStr); + connectRecord.setData(decodedData); + log.debug("Decoded Base64 data: {} bytes", decodedData.length); + } catch (IllegalArgumentException e) { + log.error("Failed to decode Base64 data: {}", e.getMessage()); + // Keep original data if decoding fails + } + } + } + + /** + * Generate a unique session ID + * + * @return Generated session ID + */ + private String generateSessionId() { + return "mcp-session-" + UUID.randomUUID().toString(); + } + + /** + * Get protocol name + * + * @return Protocol name + */ + public String getProtocolName() { + return PROTOCOL_NAME; + } +} \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/server-config.yml b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/server-config.yml index 5f66dd0f68..8009f5c76a 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/server-config.yml +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/server-config.yml @@ -16,4 +16,4 @@ # sourceEnable: true -sinkEnable: true +sinkEnable: false \ No newline at end of file From 3e5eb35c33d0f9e31d9a48eb53924dbd9339ff3c Mon Sep 17 00:00:00 2001 From: jerryyummy <461697582@qq.com> Date: Wed, 15 Oct 2025 10:47:42 -0700 Subject: [PATCH 15/36] build the basic mcp server --- .../eventmesh/connector/mcp/source/McpSourceConnector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java index 84f5d0e245..a9f72a84ad 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java @@ -219,7 +219,7 @@ private void registerDefaultTools() { } ); - // EventMesh message sending tool (example) + // EventMesh message sending tool toolRegistry.registerTool( TOOL_SEND_MESSAGE, TOOL_DESC_SEND_MESSAGE, From b948d5c049ed7065d072ceba17b076b88e1eb749 Mon Sep 17 00:00:00 2001 From: jerryyummy <461697582@qq.com> Date: Fri, 31 Oct 2025 01:12:54 -0700 Subject: [PATCH 16/36] build the basic mcp server --- .../connector/mcp/SourceConnectorConfig.java | 4 +- .../connector/mcp/sink/McpSinkConnector.java | 4 +- .../mcp/source/McpSourceConnector.java | 68 ++++++----- .../mcp/source/McpSourceConstants.java | 34 ------ .../connector/mcp/source/data/McpRequest.java | 85 +------------ .../protocol/impl/McpStandardProtocol.java | 112 ++++-------------- .../src/main/resources/source-config.yml | 3 +- 7 files changed, 70 insertions(+), 240 deletions(-) diff --git a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SourceConnectorConfig.java b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SourceConnectorConfig.java index 39faa2480d..18808942e0 100644 --- a/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SourceConnectorConfig.java +++ b/eventmesh-common/src/main/java/org/apache/eventmesh/common/config/connector/mcp/SourceConnectorConfig.java @@ -57,5 +57,7 @@ public class SourceConnectorConfig { private Map extraConfig = new HashMap<>(); // data consistency enabled, default true - private boolean dataConsistencyEnabled = true; + private boolean dataConsistencyEnabled = false; + + private String forwardPath; } diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java index ca711cad04..59bd4bbbba 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java @@ -105,7 +105,7 @@ private void doInit() { parallelism, 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<>(10000), // Built-in queue with capacity + new LinkedBlockingQueue<>(), // Built-in queue with capacity new EventMeshThreadFactory("mcp-sink-handler") ); } @@ -154,7 +154,6 @@ public void stop() throws Exception { Thread.currentThread().interrupt(); } - log.info("All tasks completed, stopping mcp sink handler"); this.sinkHandler.stop(); } @@ -170,6 +169,7 @@ public void put(List sinkRecords) { log.warn("ConnectRecord data is null, ignore."); continue; } + log.info("McpSinkConnector put record: {}", sinkRecord); try { // Use executor.submit() instead of custom queue diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java index a9f72a84ad..44c501346c 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java @@ -19,10 +19,14 @@ import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.*; +import io.vertx.core.buffer.Buffer; import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.client.WebClient; +import lombok.var; import org.apache.eventmesh.common.config.connector.Config; import org.apache.eventmesh.common.config.connector.mcp.McpSourceConfig; import org.apache.eventmesh.common.exception.EventMeshException; +import org.apache.eventmesh.connector.mcp.source.data.McpRequest; import org.apache.eventmesh.connector.mcp.source.protocol.Protocol; import org.apache.eventmesh.connector.mcp.source.protocol.ProtocolFactory; import org.apache.eventmesh.openconnect.api.ConnectorCreateService; @@ -32,7 +36,9 @@ import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -63,6 +69,8 @@ public class McpSourceConnector implements Source, ConnectorCreateService { @@ -155,9 +166,6 @@ private void doInit() { String contentType = ctx.request().getHeader(HEADER_CONTENT_TYPE); String accept = ctx.request().getHeader(HEADER_ACCEPT); - log.info("Request to {} - Content-Type: {}, Accept: {}", - basePath, contentType, accept); - // Determine if it's an SSE request or JSON-RPC request if (CONTENT_TYPE_SSE.startsWith(accept != null ? accept : "")) { handleSseRequest(ctx); @@ -168,10 +176,7 @@ private void doInit() { // GET request for SSE support router.get(basePath) - .handler(ctx -> { - log.info("GET request to {} - treating as SSE", basePath); - handleSseRequest(ctx); - }); + .handler(this::handleSseRequest); // Health check endpoint router.get(basePath + ENDPOINT_HEALTH).handler(ctx -> { @@ -184,6 +189,8 @@ private void doInit() { .end(health.encode()); }); + Route forwardRoute = router.route().path(forwardPath).handler(LoggerHandler.create()); + this.route = router.route() .path(this.sourceConfig.connectorConfig.getPath()) .handler(LoggerHandler.create()); @@ -191,6 +198,7 @@ private void doInit() { // set protocol handler this.protocol.setHandler(route, queue); + this.protocol.setHandler(forwardRoute, queue); // Create server this.server = vertx.createHttpServer(new HttpServerOptions() @@ -210,8 +218,8 @@ private void doInit() { private void registerDefaultTools() { // Echo tool toolRegistry.registerTool( - TOOL_ECHO, - TOOL_DESC_ECHO, + "echo", + "Echo back the input message", createEchoSchema(), args -> { String message = args.getString(PARAM_MESSAGE, DEFAULT_NO_MESSAGE); @@ -221,15 +229,28 @@ private void registerDefaultTools() { // EventMesh message sending tool toolRegistry.registerTool( - TOOL_SEND_MESSAGE, - TOOL_DESC_SEND_MESSAGE, + "sendEventMeshMessage", + "Send a message to EventMesh", createSendMessageSchema(), args -> { String topic = args.getString(PARAM_TOPIC); - String message = args.getString(PARAM_MESSAGE); - - // TODO: Implement actual EventMesh message sending logic - // Messages can be queued and processed by poll() method + Object message = args.getString(PARAM_MESSAGE); + + webClient.post(this.sourceConfig.connectorConfig.getPort(), "127.0.0.1", this.forwardPath) + .putHeader(CORS_EXPOSED_HEADERS, CONTENT_TYPE_JSON_PLAIN) + .sendBuffer(Buffer.buffer( + new JsonObject() + .put("type","mcp.tools.call") + .put("tool", "sendEventMeshMessage") + .put("arguments", new JsonObject().put("message", message).put("topic", topic)) + .encode() + ), ar -> { + if (ar.succeeded()) { + log.info("forwarded tools/call to {} OK, status={}", forwardPath, ar.result().statusCode()); + } else { + log.warn("forward tools/call failed: {}", ar.cause().toString()); + } + }); return createTextContent( String.format("Message sent to topic '%s': %s", topic, message) @@ -246,14 +267,12 @@ private void registerDefaultTools() { */ private void handleJsonRpcRequest(RoutingContext ctx) { String body = ctx.body().asString(); - log.info("<<< JSON-RPC Request: {}", body); try { JsonObject request = new JsonObject(body); JsonObject response = handleMcpRequest(request); if (response != null) { - log.info(">>> JSON-RPC Response: {}", response.encode()); ctx.response() .putHeader(HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON) .end(response.encode()); @@ -263,7 +282,6 @@ private void handleJsonRpcRequest(RoutingContext ctx) { } } catch (Exception e) { - log.error("Error handling JSON-RPC request", e); JsonObject error = createErrorResponse(null, ERROR_INTERNAL, "Internal error: " + e.getMessage()); ctx.response() @@ -278,8 +296,6 @@ private void handleJsonRpcRequest(RoutingContext ctx) { * @param ctx Routing context */ private void handleSseRequest(RoutingContext ctx) { - log.info("Establishing SSE connection..."); - ctx.response() .putHeader(HEADER_CONTENT_TYPE, CONTENT_TYPE_SSE) .putHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE) @@ -291,8 +307,6 @@ private void handleSseRequest(RoutingContext ctx) { ctx.response().write(SSE_EVENT_OPEN); ctx.response().write(SSE_DATA_OPEN); - log.info("SSE connection established"); - // Heartbeat (optional) long timerId = vertx.setPeriodic(DEFAULT_HEARTBEAT_INTERVAL_MS, id -> { if (!ctx.response().closed()) { @@ -303,7 +317,6 @@ private void handleSseRequest(RoutingContext ctx) { }); ctx.request().connection().closeHandler(v -> { - log.info("SSE connection closed"); vertx.cancelTimer(timerId); }); } @@ -318,13 +331,10 @@ private JsonObject handleMcpRequest(JsonObject request) { Object id = request.getValue(KEY_ID); JsonObject params = request.getJsonObject(KEY_PARAMS); - log.info("Handling MCP method: {}", method); - switch (method) { case METHOD_INITIALIZE: return handleInitialize(id, params); case METHOD_NOTIFICATIONS_INITIALIZED: - log.info("Client sent initialized notification"); return null; // Notifications don't need response case METHOD_TOOLS_LIST: return handleToolsList(id); @@ -349,8 +359,6 @@ private JsonObject handleInitialize(Object id, JsonObject params) { params.getString(KEY_PROTOCOL_VERSION, DEFAULT_PROTOCOL_VERSION) : DEFAULT_PROTOCOL_VERSION; - log.info("Initialize - Protocol version: {}", clientVersion); - JsonObject result = new JsonObject() .put(KEY_PROTOCOL_VERSION, clientVersion) .put(KEY_SERVER_INFO, new JsonObject() diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConstants.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConstants.java index c60c57276e..68cbb5fe1d 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConstants.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConstants.java @@ -388,40 +388,6 @@ private McpSourceConstants() { */ public static final String X_ACCEL_BUFFERING_NO = "no"; - // ========== Tool Names ========== - - /** - * Echo tool name - */ - public static final String TOOL_ECHO = "echo"; - - /** - * Get current timestamp tool name - */ - public static final String TOOL_GET_TIMESTAMP = "getCurrentTimestamp"; - - /** - * Send EventMesh message tool name - */ - public static final String TOOL_SEND_MESSAGE = "sendEventMeshMessage"; - - // ========== Tool Descriptions ========== - - /** - * Echo tool description - */ - public static final String TOOL_DESC_ECHO = "Echo back the input message"; - - /** - * Get timestamp tool description - */ - public static final String TOOL_DESC_TIMESTAMP = "Get current Unix timestamp in milliseconds"; - - /** - * Send message tool description - */ - public static final String TOOL_DESC_SEND_MESSAGE = "Send a message to EventMesh"; - // ========== Tool Parameter Names ========== /** diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java index 61ee2c650e..62be7849e3 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java @@ -41,7 +41,7 @@ public class McpRequest implements Serializable { private static final long serialVersionUID = -483500600756490500L; /** - * Protocol name (should be "MCP") + * Protocol name */ private String protocolName; @@ -51,22 +51,17 @@ public class McpRequest implements Serializable { private String sessionId; /** - * JSON-RPC request ID - */ - private Object requestId; - - /** - * MCP method name (e.g., "initialize", "tools/call") + * MCP method name */ private String method; /** - * Tool name (for tools/call method) + * Tool name */ private String toolName; /** - * Tool arguments (for tools/call method) + * Tool arguments */ private JsonObject arguments; @@ -90,80 +85,8 @@ public class McpRequest implements Serializable { */ private String errorMessage; - /** - * Additional metadata - */ - @Builder.Default - private Map metadata = new HashMap<>(); - - /** - * Request input data - */ - private Object inputs; - /** * Vert.x routing context for HTTP response handling */ private transient RoutingContext routingContext; - - /** - * Add a metadata entry - * - * @param key Metadata key - * @param value Metadata value - */ - public void addMetadata(String key, String value) { - if (this.metadata == null) { - this.metadata = new HashMap<>(); - } - this.metadata.put(key, value); - } - - /** - * Get metadata value by key - * - * @param key Metadata key - * @return Metadata value, or null if not found - */ - public String getMetadata(String key) { - return this.metadata != null ? this.metadata.get(key) : null; - } - - /** - * Check if this is a tool call request - * - * @return true if this is a tools/call request - */ - public boolean isToolCall() { - return "tools/call".equals(this.method); - } - - /** - * Check if this is an initialize request - * - * @return true if this is an initialize request - */ - public boolean isInitialize() { - return "initialize".equals(this.method); - } - - /** - * Convert to a simple map representation - * - * @return Map containing key request information - */ - public Map toMap() { - Map map = new HashMap<>(); - map.put("protocolName", protocolName); - map.put("sessionId", sessionId); - map.put("requestId", requestId); - map.put("method", method); - map.put("toolName", toolName); - map.put("timestamp", timestamp); - map.put("success", success); - if (errorMessage != null) { - map.put("errorMessage", errorMessage); - } - return map; - } } \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java index 9d721fa14a..a092a0981b 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java @@ -53,16 +53,16 @@ public class McpStandardProtocol implements Protocol { public static final String PROTOCOL_NAME = "MCP"; // Extension keys - private static final String EXTENSION_PROTOCOL = "protocol"; - private static final String EXTENSION_SESSION_ID = "sessionId"; - private static final String EXTENSION_TOOL_NAME = "toolName"; - private static final String EXTENSION_METHOD = "method"; - private static final String EXTENSION_REQUEST_ID = "requestId"; - private static final String EXTENSION_SUCCESS = "success"; - private static final String EXTENSION_ERROR_MESSAGE = "errorMessage"; - private static final String EXTENSION_ROUTING_CONTEXT = "routingContext"; - private static final String EXTENSION_IS_BASE64 = "isBase64"; - private static final String METADATA_EXTENSION_KEY = "extension"; + private static final String EXTENSION_PROTOCOL = "protocol"; + private static final String EXTENSION_SESSION_ID = "sessionid"; + private static final String EXTENSION_TOOL_NAME = "toolname"; + private static final String EXTENSION_METHOD = "method"; // ok + private static final String EXTENSION_REQUEST_ID = "requestid"; + private static final String EXTENSION_SUCCESS = "success"; // ok + private static final String EXTENSION_ERROR_MESSAGE = "errormessage"; + private static final String EXTENSION_ROUTING_CONTEXT = "routingcontext"; + private static final String EXTENSION_IS_BASE64 = "isbase64"; + private static final String METADATA_EXTENSION_KEY = "extension"; private SourceConnectorConfig sourceConnectorConfig; @@ -92,7 +92,6 @@ public void setHandler(Route route, BlockingQueue queue) { try { // Parse the request body String bodyString = ctx.body().asString(Constants.DEFAULT_CHARSET.toString()); - log.debug("Received MCP request: {}", bodyString); // Try to parse as JSON JsonObject requestJson; @@ -108,17 +107,9 @@ public void setHandler(Route route, BlockingQueue queue) { } // Extract JSON-RPC fields - String method = requestJson.getString("method", ""); - Object requestId = requestJson.getValue("id"); - JsonObject params = requestJson.getJsonObject("params"); - - // Extract headers as metadata - Map metadata = ctx.request().headers().entries().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - (v1, v2) -> v1 // Keep first value if duplicate keys - )); + String method = requestJson.getString("type", ""); + String toolName = requestJson.getString("tool", ""); + JsonObject params = requestJson.getJsonObject("arguments"); // Generate session ID if not present String sessionId = ctx.request().getHeader("Mcp-Session-Id"); @@ -129,11 +120,9 @@ public void setHandler(Route route, BlockingQueue queue) { // Create MCP request based on method type McpRequest mcpRequest = createMcpRequest( method, - requestId, params, sessionId, - metadata, - bodyString, + toolName, ctx ); @@ -179,41 +168,30 @@ public void setHandler(Route route, BlockingQueue queue) { * Create MCP request from parsed JSON-RPC data * * @param method JSON-RPC method name - * @param requestId JSON-RPC request ID * @param params JSON-RPC params * @param sessionId Session identifier - * @param metadata Request metadata (headers) - * @param rawBody Raw request body + * @param tool Tool name * @param ctx Routing context * @return Constructed McpRequest */ private McpRequest createMcpRequest( String method, - Object requestId, JsonObject params, String sessionId, - Map metadata, - String rawBody, + String tool, io.vertx.ext.web.RoutingContext ctx) { McpRequest.McpRequestBuilder builder = McpRequest.builder() .protocolName(PROTOCOL_NAME) .sessionId(sessionId) - .requestId(requestId) .method(method) + .toolName(tool) .timestamp(System.currentTimeMillis()) - .inputs(rawBody) .routingContext(ctx); - // Set metadata - if (metadata != null && !metadata.isEmpty()) { - builder.metadata(metadata); - } else { - builder.metadata(new HashMap<>()); - } // Handle different method types - if ("tools/call".equals(method) && params != null) { + if ("mcp.tools.call".equals(method) && params != null) { // Tool call request String toolName = params.getString("name"); JsonObject arguments = params.getJsonObject("arguments", new JsonObject()); @@ -222,16 +200,13 @@ private McpRequest createMcpRequest( .arguments(arguments) .success(false); // Will be set to true after execution - log.debug("Created tool call request: tool={}, id={}", toolName, requestId); } else if ("initialize".equals(method)) { // Initialize request - log.debug("Created initialize request: id={}", requestId); builder.success(true); } else { // Other methods - log.debug("Created generic MCP request: method={}, id={}", method, requestId); builder.success(true); } @@ -279,11 +254,6 @@ public ConnectRecord convertToConnectRecord(Object message) { connectRecord.addExtension(EXTENSION_SESSION_ID, request.getSessionId()); } - // Add request ID - if (request.getRequestId() != null) { - connectRecord.addExtension(EXTENSION_REQUEST_ID, String.valueOf(request.getRequestId())); - } - // Add method if (request.getMethod() != null) { connectRecord.addExtension(EXTENSION_METHOD, request.getMethod()); @@ -302,26 +272,6 @@ public ConnectRecord convertToConnectRecord(Object message) { connectRecord.addExtension(EXTENSION_ERROR_MESSAGE, request.getErrorMessage()); } - // Add metadata from request - if (request.getMetadata() != null) { - request.getMetadata().forEach((key, value) -> { - try { - // Handle nested extension objects - if (METADATA_EXTENSION_KEY.equalsIgnoreCase(key)) { - JsonObject extension = new JsonObject(value); - extension.forEach(entry -> - connectRecord.addExtension(entry.getKey(), entry.getValue()) - ); - } else { - connectRecord.addExtension(key, value); - } - } catch (Exception e) { - log.warn("Failed to add metadata: key={}, error={}", key, e.getMessage()); - connectRecord.addExtension(key, value); - } - }); - } - // Handle Base64 decoding if needed handleBase64Decoding(connectRecord); @@ -330,9 +280,6 @@ public ConnectRecord convertToConnectRecord(Object message) { connectRecord.addExtension(EXTENSION_ROUTING_CONTEXT, request.getRoutingContext()); } - log.debug("Converted MCP request to ConnectRecord: method={}, tool={}, id={}", - request.getMethod(), request.getToolName(), request.getRequestId()); - return connectRecord; } @@ -341,24 +288,16 @@ public ConnectRecord convertToConnectRecord(Object message) { * Priority: result > arguments > inputs */ private Object extractData(McpRequest request) { - // 1. If tool execution succeeded, use result if (request.isSuccess() && request.getResult() != null) { return request.getResult().encode(); } - // 2. If tool call, use arguments if (request.getArguments() != null) { return request.getArguments().encode(); } - // 3. Use inputs (raw request body) - if (request.getInputs() != null) { - return request.getInputs(); - } - - // 4. Fallback: create minimal info - return String.format("{\"method\":\"%s\",\"timestamp\":%d}", - request.getMethod(), request.getTimestamp()); + return String.format("{\"tool\":\"%s\",\"timestamp\":%d}", + request.getToolName(), request.getTimestamp()); } /** @@ -399,15 +338,6 @@ private void handleBase64Decoding(ConnectRecord connectRecord) { * @return Generated session ID */ private String generateSessionId() { - return "mcp-session-" + UUID.randomUUID().toString(); - } - - /** - * Get protocol name - * - * @return Protocol name - */ - public String getProtocolName() { - return PROTOCOL_NAME; + return "mcp-session-" + UUID.randomUUID(); } } \ No newline at end of file diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/source-config.yml b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/source-config.yml index 4bd63254a7..66ad75d37b 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/source-config.yml +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/resources/source-config.yml @@ -34,4 +34,5 @@ connectorConfig: extraConfig: # extra config for different protocol, e.g. GitHub secret streamType: chunked contentType: application/json - reconnection: true \ No newline at end of file + reconnection: true + forwardPath: /forward \ No newline at end of file From cdcef32543e5db9a413c1e2a969a3ba1b141ef98 Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 20:44:12 +0800 Subject: [PATCH 17/36] Update RemoteSubscribeInstance.java --- .../demo/sub/RemoteSubscribeInstance.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/eventmesh-examples/src/main/java/org/apache/eventmesh/http/demo/sub/RemoteSubscribeInstance.java b/eventmesh-examples/src/main/java/org/apache/eventmesh/http/demo/sub/RemoteSubscribeInstance.java index 55f79d2248..f045bf077b 100644 --- a/eventmesh-examples/src/main/java/org/apache/eventmesh/http/demo/sub/RemoteSubscribeInstance.java +++ b/eventmesh-examples/src/main/java/org/apache/eventmesh/http/demo/sub/RemoteSubscribeInstance.java @@ -17,20 +17,6 @@ package org.apache.eventmesh.http.demo.sub; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.util.EntityUtils; - import org.apache.eventmesh.client.http.EventMeshRetObj; import org.apache.eventmesh.client.http.model.RequestParam; import org.apache.eventmesh.client.http.util.HttpUtils; @@ -46,6 +32,20 @@ import org.apache.eventmesh.common.utils.IPUtils; import org.apache.eventmesh.common.utils.JsonUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + import io.netty.handler.codec.http.HttpMethod; import lombok.extern.slf4j.Slf4j; From e86af6341b38447d9a286fd3bca61607a734f135 Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 20:54:50 +0800 Subject: [PATCH 18/36] Update McpSinkHandlerRetryWrapper.java --- .../handler/impl/McpSinkHandlerRetryWrapper.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java index e917585752..52a935fbe6 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java @@ -6,7 +6,7 @@ * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * - * Mcp://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -17,12 +17,6 @@ package org.apache.eventmesh.connector.mcp.sink.handler.impl; -import dev.failsafe.Failsafe; -import dev.failsafe.RetryPolicy; -import io.vertx.core.Future; -import io.vertx.core.buffer.Buffer; -import io.vertx.ext.web.client.HttpResponse; -import lombok.extern.slf4j.Slf4j; import org.apache.eventmesh.common.config.connector.mcp.McpRetryConfig; import org.apache.eventmesh.common.config.connector.mcp.SinkConnectorConfig; import org.apache.eventmesh.connector.http.util.HttpUtils; @@ -36,6 +30,12 @@ import java.time.Duration; import java.util.Map; +import dev.failsafe.Failsafe; +import dev.failsafe.RetryPolicy; +import io.vertx.core.Future; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.client.HttpResponse; +import lombok.extern.slf4j.Slf4j; /** * McpSinkHandlerRetryWrapper is a wrapper class for the McpSinkHandler that provides retry functionality for failed Mcp requests. From 358f49154d61b142f1f9e60ef0f92f40147cb807 Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 21:13:19 +0800 Subject: [PATCH 19/36] Update CommonMcpSinkHandler.java --- .../handler/impl/CommonMcpSinkHandler.java | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java index cec2d8591c..1d884f4a19 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/CommonMcpSinkHandler.java @@ -17,17 +17,6 @@ package org.apache.eventmesh.connector.mcp.sink.handler.impl; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.vertx.core.Future; -import io.vertx.core.MultiMap; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpHeaders; -import io.vertx.ext.web.client.HttpResponse; -import io.vertx.ext.web.client.WebClient; -import io.vertx.ext.web.client.WebClientOptions; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; import org.apache.eventmesh.common.config.connector.mcp.SinkConnectorConfig; import org.apache.eventmesh.common.utils.JsonUtils; import org.apache.eventmesh.connector.http.util.HttpUtils; @@ -47,6 +36,19 @@ import java.util.Set; import java.util.concurrent.TimeUnit; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpHeaders; +import io.vertx.ext.web.client.HttpResponse; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + /** * Common MCP Sink Handler implementation to handle ConnectRecords by sending them over MCP to configured URLs. @@ -266,4 +268,4 @@ public void stop() { log.warn("WebClient is null, ignore."); } } -} \ No newline at end of file +} From 8cd5d08dbe5a44427166c9e9f5ff56e75f8df47a Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 21:14:48 +0800 Subject: [PATCH 20/36] Update McpSinkConnector.java --- .../eventmesh/connector/mcp/sink/McpSinkConnector.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java index 59bd4bbbba..3d65fb9b5d 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/McpSinkConnector.java @@ -17,9 +17,6 @@ package org.apache.eventmesh.connector.mcp.sink; -import lombok.Getter; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; import org.apache.eventmesh.common.EventMeshThreadFactory; import org.apache.eventmesh.common.config.connector.Config; import org.apache.eventmesh.common.config.connector.mcp.McpSinkConfig; @@ -40,6 +37,10 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + @Slf4j public class McpSinkConnector implements Sink, ConnectorCreateService { @@ -185,4 +186,4 @@ public void put(List sinkRecords) { } } } -} \ No newline at end of file +} From 180c39fecda82bc94ba0fa4c7724227344dcb64e Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 21:16:41 +0800 Subject: [PATCH 21/36] Update McpSinkHandler.java --- .../connector/mcp/sink/handler/McpSinkHandler.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java index 50f0cc318d..c10d96337b 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/McpSinkHandler.java @@ -17,15 +17,16 @@ package org.apache.eventmesh.connector.mcp.sink.handler; -import io.vertx.core.Future; -import io.vertx.core.buffer.Buffer; -import io.vertx.ext.web.client.HttpResponse; import org.apache.eventmesh.connector.mcp.sink.data.McpConnectRecord; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; import java.net.URI; import java.util.Map; +import io.vertx.core.Future; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.client.HttpResponse; + /** * Interface for handling ConnectRecords via HTTP or HTTPS. Classes implementing this interface are responsible for processing ConnectRecords by * sending them over HTTP or HTTPS, with additional support for handling multiple requests and asynchronous processing. From bc7035b4be1c808fcead8a1cdbf7607529932485 Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 21:20:14 +0800 Subject: [PATCH 22/36] Update McpExportRecord.java --- .../eventmesh/connector/mcp/sink/data/McpExportRecord.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java index 8a9d479437..c9a35c193b 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecord.java @@ -17,11 +17,11 @@ package org.apache.eventmesh.connector.mcp.sink.data; +import java.io.Serializable; + import lombok.AllArgsConstructor; import lombok.Data; -import java.io.Serializable; - /** * Represents an MCP export record containing metadata and data to be exported. */ From f620f01be967a0b9d3ccfdf9a9102ac0f800c0ec Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 21:21:09 +0800 Subject: [PATCH 23/36] Update McpConnectRecord.java --- .../eventmesh/connector/mcp/sink/data/McpConnectRecord.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java index ea4e34aec3..26f9c1e6fb 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpConnectRecord.java @@ -17,8 +17,6 @@ package org.apache.eventmesh.connector.mcp.sink.data; -import lombok.Builder; -import lombok.Getter; import org.apache.eventmesh.common.remote.offset.http.HttpRecordOffset; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.KeyValue; @@ -30,6 +28,9 @@ import java.util.Map; import java.util.UUID; +import lombok.Builder; +import lombok.Getter; + /** * a special ConnectRecord for McpSinkConnector */ From 34956e057095b18baf17a20368c46851a8fb4a51 Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 21:22:00 +0800 Subject: [PATCH 24/36] Update McpExportRecordPage.java --- .../connector/mcp/sink/data/McpExportRecordPage.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java index dfbdccbd4b..3e0e615523 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportRecordPage.java @@ -17,12 +17,12 @@ package org.apache.eventmesh.connector.mcp.sink.data; -import lombok.AllArgsConstructor; -import lombok.Data; - import java.io.Serializable; import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; + /** * Represents a page of MCP export records. */ From 51825d05cd4e1f0c78af498abb48ebe586aa0767 Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 21:24:10 +0800 Subject: [PATCH 25/36] Update McpExportMetadata.java --- .../connector/mcp/sink/data/McpExportMetadata.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java index 7acdfddf86..72595b2637 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/data/McpExportMetadata.java @@ -17,12 +17,12 @@ package org.apache.eventmesh.connector.mcp.sink.data; -import lombok.Builder; -import lombok.Data; - import java.io.Serializable; import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Data; + /** * Metadata for an MCP export operation. */ From 9e767988111d7b0d63facd1e7c3ccbf26babfebc Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 21:57:18 +0800 Subject: [PATCH 26/36] Update McpSourceConnector.java --- .../connector/mcp/source/McpSourceConnector.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java index 44c501346c..3a8d7a7a89 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java @@ -19,10 +19,6 @@ import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.*; -import io.vertx.core.buffer.Buffer; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.client.WebClient; -import lombok.var; import org.apache.eventmesh.common.config.connector.Config; import org.apache.eventmesh.common.config.connector.mcp.McpSourceConfig; import org.apache.eventmesh.common.exception.EventMeshException; @@ -43,18 +39,22 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import io.vertx.core.buffer.Buffer; import io.vertx.core.Vertx; import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.client.WebClient; import io.vertx.ext.web.Route; import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.BodyHandler; import io.vertx.ext.web.handler.LoggerHandler; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import lombok.var; /** * MCP Source Connector for EventMesh @@ -579,4 +579,4 @@ public List poll() { } return connectRecords; } -} \ No newline at end of file +} From 43c297fe02b69ed88118940616ce446da8ce1a8d Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 21:58:34 +0800 Subject: [PATCH 27/36] Update McpToolRegistry.java --- .../connector/mcp/source/McpToolRegistry.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpToolRegistry.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpToolRegistry.java index 0d127874a5..a59d93f0b6 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpToolRegistry.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpToolRegistry.java @@ -1,5 +1,3 @@ -package org.apache.eventmesh.connector.mcp.source; - /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with @@ -17,13 +15,16 @@ * limitations under the License. */ -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; -import lombok.extern.slf4j.Slf4j; +package org.apache.eventmesh.connector.mcp.source; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +import lombok.extern.slf4j.Slf4j; + /** * MCP Tool Registry * Manages all available MCP tools @@ -134,4 +135,4 @@ private static class McpTool { this.executor = executor; } } -} \ No newline at end of file +} From 28990a7db300500fce28c605c7cadb0d03df07af Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 22:33:30 +0800 Subject: [PATCH 28/36] Update McpServerConfig.java --- .../apache/eventmesh/connector/mcp/config/McpServerConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/config/McpServerConfig.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/config/McpServerConfig.java index 9d325e1c38..33357b6a29 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/config/McpServerConfig.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/config/McpServerConfig.java @@ -18,9 +18,10 @@ package org.apache.eventmesh.connector.mcp.config; +import org.apache.eventmesh.common.config.connector.Config; + import lombok.Data; import lombok.EqualsAndHashCode; -import org.apache.eventmesh.common.config.connector.Config; @Data From 4905170aaf3294867090e2976fdd8c0817f30275 Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 22:35:41 +0800 Subject: [PATCH 29/36] Update Protocol.java --- .../eventmesh/connector/mcp/source/protocol/Protocol.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java index d92669b769..f18397a6c3 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/Protocol.java @@ -17,12 +17,13 @@ package org.apache.eventmesh.connector.mcp.source.protocol; -import io.vertx.ext.web.Route; import org.apache.eventmesh.common.config.connector.mcp.SourceConnectorConfig; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; import java.util.concurrent.BlockingQueue; +import io.vertx.ext.web.Route; + /** * Protocol Interface. * All protocols should implement this interface. @@ -53,4 +54,4 @@ public interface Protocol { * @return ConnectRecord */ ConnectRecord convertToConnectRecord(Object message); -} \ No newline at end of file +} From 308532271c9ab04a31b6e0d9ae49f9327f23e18f Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 22:42:17 +0800 Subject: [PATCH 30/36] Update McpSourceConnector.java --- .../mcp/source/McpSourceConnector.java | 349 +++++++++++------- 1 file changed, 213 insertions(+), 136 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java index 3a8d7a7a89..e5fe258124 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConnector.java @@ -1,5 +1,3 @@ -package org.apache.eventmesh.connector.mcp.source; - /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with @@ -17,12 +15,86 @@ * limitations under the License. */ -import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.*; +package org.apache.eventmesh.connector.mcp.source; + +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.CACHE_CONTROL_NO_CACHE; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.CONNECTION_KEEP_ALIVE; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.CONTENT_TYPE_JSON; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.CONTENT_TYPE_JSON_PLAIN; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.CONTENT_TYPE_SSE; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.CORS_ALLOWED_HEADERS; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.CORS_ALLOWED_METHODS; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.CORS_ALLOW_ALL; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.CORS_EXPOSED_HEADERS; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.DEFAULT_CONNECTOR_NAME; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.DEFAULT_HEARTBEAT_INTERVAL_MS; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.DEFAULT_IDLE_TIMEOUT_MS; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.DEFAULT_NO_MESSAGE; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.DEFAULT_PROTOCOL_VERSION; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.DEFAULT_SERVER_NAME; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.DEFAULT_SERVER_VERSION; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.ENDPOINT_HEALTH; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.ERROR_INTERNAL; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.ERROR_INVALID_PARAMS; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.ERROR_METHOD_NOT_FOUND; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.HEADER_ACCEPT; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.HEADER_CACHE_CONTROL; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.HEADER_CONNECTION; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.HEADER_CONTENT_TYPE; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.HEADER_CORS_ALLOW_HEADERS; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.HEADER_CORS_ALLOW_METHODS; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.HEADER_CORS_ALLOW_ORIGIN; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.HEADER_CORS_EXPOSE_HEADERS; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.HEADER_X_ACCEL_BUFFERING; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.HTTP_METHOD_OPTIONS; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.HTTP_STATUS_INTERNAL_ERROR; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.HTTP_STATUS_NO_CONTENT; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_CAPABILITIES; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_CONNECTOR; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_CONTENT; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_DESCRIPTION; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_ERROR; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_ERROR_CODE; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_ERROR_MESSAGE; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_ID; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_JSONRPC; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_METHOD; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_NAME; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_PARAMS; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_PROPERTIES; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_PROTOCOL_VERSION; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_REQUIRED; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_RESULT; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_SERVER_INFO; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_STATUS; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_TEXT; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_TOOLS; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_TYPE; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.KEY_VERSION; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.MAX_POLL_WAIT_TIME_MS; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.METHOD_INITIALIZE; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.METHOD_NOTIFICATIONS_INITIALIZED; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.METHOD_PING; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.METHOD_TOOLS_CALL; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.METHOD_TOOLS_LIST; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.PARAM_DESC_MESSAGE; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.PARAM_DESC_MESSAGE_CONTENT; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.PARAM_DESC_TOPIC; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.PARAM_MESSAGE; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.PARAM_TOPIC; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.SSE_DATA_OPEN; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.SSE_EVENT_OPEN; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.SSE_HEARTBEAT; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.VALUE_JSONRPC_VERSION; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.VALUE_STATUS_UP; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.VALUE_TYPE_OBJECT; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.VALUE_TYPE_STRING; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.VALUE_TYPE_TEXT; +import static org.apache.eventmesh.connector.mcp.source.McpSourceConstants.X_ACCEL_BUFFERING_NO; import org.apache.eventmesh.common.config.connector.Config; import org.apache.eventmesh.common.config.connector.mcp.McpSourceConfig; import org.apache.eventmesh.common.exception.EventMeshException; -import org.apache.eventmesh.connector.mcp.source.data.McpRequest; import org.apache.eventmesh.connector.mcp.source.protocol.Protocol; import org.apache.eventmesh.connector.mcp.source.protocol.ProtocolFactory; import org.apache.eventmesh.openconnect.api.ConnectorCreateService; @@ -32,33 +104,29 @@ import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import io.vertx.core.buffer.Buffer; import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.client.WebClient; import io.vertx.ext.web.Route; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.client.WebClient; import io.vertx.ext.web.handler.BodyHandler; import io.vertx.ext.web.handler.LoggerHandler; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import lombok.var; /** - * MCP Source Connector for EventMesh - * Implements MCP protocol server allowing AI clients to interact with EventMesh via MCP protocol + * MCP Source Connector for EventMesh Implements MCP protocol server allowing AI clients to interact with EventMesh via MCP protocol */ @Slf4j public class McpSourceConnector implements Source, ConnectorCreateService { @@ -137,17 +205,16 @@ private void doInit() { final Router router = Router.router(vertx); this.webClient = WebClient.create(vertx); - final String basePath = this.sourceConfig.connectorConfig.getPath(); this.forwardPath = this.sourceConfig.connectorConfig.getForwardPath(); // Configure CORS (must be before all routes) router.route().handler(ctx -> { ctx.response() - .putHeader(HEADER_CORS_ALLOW_ORIGIN, CORS_ALLOW_ALL) - .putHeader(HEADER_CORS_ALLOW_METHODS, CORS_ALLOWED_METHODS) - .putHeader(HEADER_CORS_ALLOW_HEADERS, CORS_ALLOWED_HEADERS) - .putHeader(HEADER_CORS_EXPOSE_HEADERS, CORS_EXPOSED_HEADERS); + .putHeader(HEADER_CORS_ALLOW_ORIGIN, CORS_ALLOW_ALL) + .putHeader(HEADER_CORS_ALLOW_METHODS, CORS_ALLOWED_METHODS) + .putHeader(HEADER_CORS_ALLOW_HEADERS, CORS_ALLOWED_HEADERS) + .putHeader(HEADER_CORS_EXPOSE_HEADERS, CORS_EXPOSED_HEADERS); if (HTTP_METHOD_OPTIONS.equals(ctx.request().method().name())) { ctx.response().setStatusCode(HTTP_STATUS_NO_CONTENT).end(); @@ -161,40 +228,39 @@ private void doInit() { // Main endpoint - handles both JSON-RPC and SSE requests router.post(basePath) - .handler(LoggerHandler.create()) - .handler(ctx -> { - String contentType = ctx.request().getHeader(HEADER_CONTENT_TYPE); - String accept = ctx.request().getHeader(HEADER_ACCEPT); - - // Determine if it's an SSE request or JSON-RPC request - if (CONTENT_TYPE_SSE.startsWith(accept != null ? accept : "")) { - handleSseRequest(ctx); - } else { - handleJsonRpcRequest(ctx); - } - }); + .handler(LoggerHandler.create()) + .handler(ctx -> { + String contentType = ctx.request().getHeader(HEADER_CONTENT_TYPE); + String accept = ctx.request().getHeader(HEADER_ACCEPT); + + // Determine if it's an SSE request or JSON-RPC request + if (CONTENT_TYPE_SSE.startsWith(accept != null ? accept : "")) { + handleSseRequest(ctx); + } else { + handleJsonRpcRequest(ctx); + } + }); // GET request for SSE support router.get(basePath) - .handler(this::handleSseRequest); + .handler(this::handleSseRequest); // Health check endpoint router.get(basePath + ENDPOINT_HEALTH).handler(ctx -> { JsonObject health = new JsonObject() - .put(KEY_STATUS, VALUE_STATUS_UP) - .put(KEY_CONNECTOR, DEFAULT_CONNECTOR_NAME) - .put(KEY_TOOLS, toolRegistry.getToolCount()); + .put(KEY_STATUS, VALUE_STATUS_UP) + .put(KEY_CONNECTOR, DEFAULT_CONNECTOR_NAME) + .put(KEY_TOOLS, toolRegistry.getToolCount()); ctx.response() - .putHeader(HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON) - .end(health.encode()); + .putHeader(HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON) + .end(health.encode()); }); Route forwardRoute = router.route().path(forwardPath).handler(LoggerHandler.create()); this.route = router.route() - .path(this.sourceConfig.connectorConfig.getPath()) - .handler(LoggerHandler.create()); - + .path(this.sourceConfig.connectorConfig.getPath()) + .handler(LoggerHandler.create()); // set protocol handler this.protocol.setHandler(route, queue); @@ -202,14 +268,14 @@ private void doInit() { // Create server this.server = vertx.createHttpServer(new HttpServerOptions() - .setPort(this.sourceConfig.connectorConfig.getPort()) - .setHandle100ContinueAutomatically(true) - .setIdleTimeout(DEFAULT_IDLE_TIMEOUT_MS) - .setIdleTimeoutUnit(TimeUnit.MILLISECONDS)) - .requestHandler(router); + .setPort(this.sourceConfig.connectorConfig.getPort()) + .setHandle100ContinueAutomatically(true) + .setIdleTimeout(DEFAULT_IDLE_TIMEOUT_MS) + .setIdleTimeoutUnit(TimeUnit.MILLISECONDS)) + .requestHandler(router); log.info("MCP Source Connector initialized on http://127.0.0.1:{}{}", - this.sourceConfig.connectorConfig.getPort(), basePath); + this.sourceConfig.connectorConfig.getPort(), basePath); } /** @@ -218,44 +284,44 @@ private void doInit() { private void registerDefaultTools() { // Echo tool toolRegistry.registerTool( - "echo", - "Echo back the input message", - createEchoSchema(), - args -> { - String message = args.getString(PARAM_MESSAGE, DEFAULT_NO_MESSAGE); - return createTextContent("Echo: " + message); - } + "echo", + "Echo back the input message", + createEchoSchema(), + args -> { + String message = args.getString(PARAM_MESSAGE, DEFAULT_NO_MESSAGE); + return createTextContent("Echo: " + message); + } ); // EventMesh message sending tool toolRegistry.registerTool( - "sendEventMeshMessage", - "Send a message to EventMesh", - createSendMessageSchema(), - args -> { - String topic = args.getString(PARAM_TOPIC); - Object message = args.getString(PARAM_MESSAGE); - - webClient.post(this.sourceConfig.connectorConfig.getPort(), "127.0.0.1", this.forwardPath) - .putHeader(CORS_EXPOSED_HEADERS, CONTENT_TYPE_JSON_PLAIN) - .sendBuffer(Buffer.buffer( - new JsonObject() - .put("type","mcp.tools.call") - .put("tool", "sendEventMeshMessage") - .put("arguments", new JsonObject().put("message", message).put("topic", topic)) - .encode() - ), ar -> { - if (ar.succeeded()) { - log.info("forwarded tools/call to {} OK, status={}", forwardPath, ar.result().statusCode()); - } else { - log.warn("forward tools/call failed: {}", ar.cause().toString()); - } - }); - - return createTextContent( - String.format("Message sent to topic '%s': %s", topic, message) - ); - } + "sendEventMeshMessage", + "Send a message to EventMesh", + createSendMessageSchema(), + args -> { + String topic = args.getString(PARAM_TOPIC); + Object message = args.getString(PARAM_MESSAGE); + + webClient.post(this.sourceConfig.connectorConfig.getPort(), "127.0.0.1", this.forwardPath) + .putHeader(CORS_EXPOSED_HEADERS, CONTENT_TYPE_JSON_PLAIN) + .sendBuffer(Buffer.buffer( + new JsonObject() + .put("type", "mcp.tools.call") + .put("tool", "sendEventMeshMessage") + .put("arguments", new JsonObject().put("message", message).put("topic", topic)) + .encode() + ), ar -> { + if (ar.succeeded()) { + log.info("forwarded tools/call to {} OK, status={}", forwardPath, ar.result().statusCode()); + } else { + log.warn("forward tools/call failed: {}", ar.cause().toString()); + } + }); + + return createTextContent( + String.format("Message sent to topic '%s': %s", topic, message) + ); + } ); log.info("Registered {} MCP tools", toolRegistry.getToolCount()); @@ -263,6 +329,7 @@ private void registerDefaultTools() { /** * Handle JSON-RPC request (HTTP mode) + * * @param ctx Routing context */ private void handleJsonRpcRequest(RoutingContext ctx) { @@ -274,8 +341,8 @@ private void handleJsonRpcRequest(RoutingContext ctx) { if (response != null) { ctx.response() - .putHeader(HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON) - .end(response.encode()); + .putHeader(HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON) + .end(response.encode()); } else { // Notification messages don't need response ctx.response().setStatusCode(HTTP_STATUS_NO_CONTENT).end(); @@ -283,25 +350,26 @@ private void handleJsonRpcRequest(RoutingContext ctx) { } catch (Exception e) { JsonObject error = createErrorResponse(null, ERROR_INTERNAL, - "Internal error: " + e.getMessage()); + "Internal error: " + e.getMessage()); ctx.response() - .putHeader(HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON) - .setStatusCode(HTTP_STATUS_INTERNAL_ERROR) - .end(error.encode()); + .putHeader(HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON) + .setStatusCode(HTTP_STATUS_INTERNAL_ERROR) + .end(error.encode()); } } /** * Handle SSE request (Server-Sent Events mode) + * * @param ctx Routing context */ private void handleSseRequest(RoutingContext ctx) { ctx.response() - .putHeader(HEADER_CONTENT_TYPE, CONTENT_TYPE_SSE) - .putHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE) - .putHeader(HEADER_CONNECTION, CONNECTION_KEEP_ALIVE) - .putHeader(HEADER_X_ACCEL_BUFFERING, X_ACCEL_BUFFERING_NO) - .setChunked(true); + .putHeader(HEADER_CONTENT_TYPE, CONTENT_TYPE_SSE) + .putHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE) + .putHeader(HEADER_CONNECTION, CONNECTION_KEEP_ALIVE) + .putHeader(HEADER_X_ACCEL_BUFFERING, X_ACCEL_BUFFERING_NO) + .setChunked(true); // Send connection established event ctx.response().write(SSE_EVENT_OPEN); @@ -323,6 +391,7 @@ private void handleSseRequest(RoutingContext ctx) { /** * Handle MCP JSON-RPC request + * * @param request JSON-RPC request object * @return JSON-RPC response object, or null for notifications */ @@ -344,34 +413,36 @@ private JsonObject handleMcpRequest(JsonObject request) { return createSuccessResponse(id, new JsonObject()); default: return createErrorResponse(id, ERROR_METHOD_NOT_FOUND, - "Method not found: " + method); + "Method not found: " + method); } } /** * Handle initialize method - * @param id Request ID + * + * @param id Request ID * @param params Request parameters * @return Initialize response */ private JsonObject handleInitialize(Object id, JsonObject params) { - String clientVersion = params != null ? - params.getString(KEY_PROTOCOL_VERSION, DEFAULT_PROTOCOL_VERSION) - : DEFAULT_PROTOCOL_VERSION; + String clientVersion = params != null + ? params.getString(KEY_PROTOCOL_VERSION, DEFAULT_PROTOCOL_VERSION) + : DEFAULT_PROTOCOL_VERSION; JsonObject result = new JsonObject() - .put(KEY_PROTOCOL_VERSION, clientVersion) - .put(KEY_SERVER_INFO, new JsonObject() - .put(KEY_NAME, DEFAULT_SERVER_NAME) - .put(KEY_VERSION, DEFAULT_SERVER_VERSION)) - .put(KEY_CAPABILITIES, new JsonObject() - .put(KEY_TOOLS, new JsonObject())); + .put(KEY_PROTOCOL_VERSION, clientVersion) + .put(KEY_SERVER_INFO, new JsonObject() + .put(KEY_NAME, DEFAULT_SERVER_NAME) + .put(KEY_VERSION, DEFAULT_SERVER_VERSION)) + .put(KEY_CAPABILITIES, new JsonObject() + .put(KEY_TOOLS, new JsonObject())); return createSuccessResponse(id, result); } /** * Handle tools/list method + * * @param id Request ID * @return Tools list response */ @@ -383,7 +454,8 @@ private JsonObject handleToolsList(Object id) { /** * Handle tools/call method - * @param id Request ID + * + * @param id Request ID * @param params Tool call parameters * @return Tool execution result */ @@ -400,7 +472,7 @@ private JsonObject handleToolsCall(Object id, JsonObject params) { try { JsonObject content = toolRegistry.executeTool(toolName, arguments); JsonObject result = new JsonObject() - .put(KEY_CONTENT, new JsonArray().add(content)); + .put(KEY_CONTENT, new JsonArray().add(content)); return createSuccessResponse(id, result); @@ -409,7 +481,7 @@ private JsonObject handleToolsCall(Object id, JsonObject params) { } catch (Exception e) { log.error("Tool execution error", e); return createErrorResponse(id, ERROR_INTERNAL, - "Tool execution failed: " + e.getMessage()); + "Tool execution failed: " + e.getMessage()); } } @@ -417,75 +489,80 @@ private JsonObject handleToolsCall(Object id, JsonObject params) { /** * Create a success response - * @param id Request ID + * + * @param id Request ID * @param result Result object * @return JSON-RPC success response */ private JsonObject createSuccessResponse(Object id, JsonObject result) { return new JsonObject() - .put(KEY_JSONRPC, VALUE_JSONRPC_VERSION) - .put(KEY_ID, id) - .put(KEY_RESULT, result); + .put(KEY_JSONRPC, VALUE_JSONRPC_VERSION) + .put(KEY_ID, id) + .put(KEY_RESULT, result); } /** * Create an error response - * @param id Request ID - * @param code Error code + * + * @param id Request ID + * @param code Error code * @param message Error message * @return JSON-RPC error response */ private JsonObject createErrorResponse(Object id, int code, String message) { return new JsonObject() - .put(KEY_JSONRPC, VALUE_JSONRPC_VERSION) - .put(KEY_ID, id) - .put(KEY_ERROR, new JsonObject() - .put(KEY_ERROR_CODE, code) - .put(KEY_ERROR_MESSAGE, message)); + .put(KEY_JSONRPC, VALUE_JSONRPC_VERSION) + .put(KEY_ID, id) + .put(KEY_ERROR, new JsonObject() + .put(KEY_ERROR_CODE, code) + .put(KEY_ERROR_MESSAGE, message)); } // ========== Schema Creation Helpers ========== /** * Create JSON schema for echo tool + * * @return Echo tool input schema */ private JsonObject createEchoSchema() { return new JsonObject() - .put(KEY_TYPE, VALUE_TYPE_OBJECT) - .put(KEY_PROPERTIES, new JsonObject() - .put(PARAM_MESSAGE, new JsonObject() - .put(KEY_TYPE, VALUE_TYPE_STRING) - .put(KEY_DESCRIPTION, PARAM_DESC_MESSAGE))) - .put(KEY_REQUIRED, new JsonArray().add(PARAM_MESSAGE)); + .put(KEY_TYPE, VALUE_TYPE_OBJECT) + .put(KEY_PROPERTIES, new JsonObject() + .put(PARAM_MESSAGE, new JsonObject() + .put(KEY_TYPE, VALUE_TYPE_STRING) + .put(KEY_DESCRIPTION, PARAM_DESC_MESSAGE))) + .put(KEY_REQUIRED, new JsonArray().add(PARAM_MESSAGE)); } /** * Create JSON schema for send message tool + * * @return Send message tool input schema */ private JsonObject createSendMessageSchema() { return new JsonObject() - .put(KEY_TYPE, VALUE_TYPE_OBJECT) - .put(KEY_PROPERTIES, new JsonObject() - .put(PARAM_TOPIC, new JsonObject() - .put(KEY_TYPE, VALUE_TYPE_STRING) - .put(KEY_DESCRIPTION, PARAM_DESC_TOPIC)) - .put(PARAM_MESSAGE, new JsonObject() - .put(KEY_TYPE, VALUE_TYPE_STRING) - .put(KEY_DESCRIPTION, PARAM_DESC_MESSAGE_CONTENT))) - .put(KEY_REQUIRED, new JsonArray().add(PARAM_TOPIC).add(PARAM_MESSAGE)); + .put(KEY_TYPE, VALUE_TYPE_OBJECT) + .put(KEY_PROPERTIES, new JsonObject() + .put(PARAM_TOPIC, new JsonObject() + .put(KEY_TYPE, VALUE_TYPE_STRING) + .put(KEY_DESCRIPTION, PARAM_DESC_TOPIC)) + .put(PARAM_MESSAGE, new JsonObject() + .put(KEY_TYPE, VALUE_TYPE_STRING) + .put(KEY_DESCRIPTION, PARAM_DESC_MESSAGE_CONTENT))) + .put(KEY_REQUIRED, new JsonArray().add(PARAM_TOPIC).add(PARAM_MESSAGE)); } /** * Create text content object + * * @param text Text content * @return MCP text content object */ private JsonObject createTextContent(String text) { return new JsonObject() - .put(KEY_TYPE, VALUE_TYPE_TEXT) - .put(KEY_TEXT, text); + .put(KEY_TYPE, VALUE_TYPE_TEXT) + .put(KEY_TEXT, text); } // ========== Source Interface Implementation ========== @@ -496,15 +573,15 @@ public void start() { if (res.succeeded()) { this.started = true; log.info("McpSourceConnector started on port: {}", - this.sourceConfig.getConnectorConfig().getPort()); + this.sourceConfig.getConnectorConfig().getPort()); log.info("MCP endpoints available at:"); log.info(" - POST {} (JSON-RPC)", this.sourceConfig.connectorConfig.getPath()); log.info(" - GET {} (SSE)", this.sourceConfig.connectorConfig.getPath()); log.info(" - GET {}{} (Health check)", - this.sourceConfig.connectorConfig.getPath(), ENDPOINT_HEALTH); + this.sourceConfig.connectorConfig.getPath(), ENDPOINT_HEALTH); } else { log.error("McpSourceConnector failed to start on port: {}", - this.sourceConfig.getConnectorConfig().getPort()); + this.sourceConfig.getConnectorConfig().getPort()); throw new EventMeshException("failed to start Vertx server", res.cause()); } }); @@ -536,10 +613,10 @@ public void stop() { if (res.succeeded()) { this.destroyed = true; log.info("McpSourceConnector stopped on port: {}", - this.sourceConfig.getConnectorConfig().getPort()); + this.sourceConfig.getConnectorConfig().getPort()); } else { log.error("McpSourceConnector failed to stop on port: {}", - this.sourceConfig.getConnectorConfig().getPort()); + this.sourceConfig.getConnectorConfig().getPort()); throw new EventMeshException("failed to stop Vertx server", res.cause()); } }); @@ -571,7 +648,7 @@ public List poll() { long elapsedTime = System.currentTimeMillis() - startTime; remainingTime = MAX_POLL_WAIT_TIME_MS > elapsedTime - ? MAX_POLL_WAIT_TIME_MS - elapsedTime : 0; + ? MAX_POLL_WAIT_TIME_MS - elapsedTime : 0; } catch (Exception e) { log.error("Failed to poll from queue.", e); throw new RuntimeException(e); From 6b50b2b2a3e00396de9d307f501158d43659c8f1 Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 22:43:03 +0800 Subject: [PATCH 31/36] Update McpSourceConstants.java --- .../eventmesh/connector/mcp/source/McpSourceConstants.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConstants.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConstants.java index 68cbb5fe1d..423cbbc2c7 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConstants.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/McpSourceConstants.java @@ -1,5 +1,3 @@ -package org.apache.eventmesh.connector.mcp.source; - /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with @@ -17,6 +15,8 @@ * limitations under the License. */ +package org.apache.eventmesh.connector.mcp.source; + /** * Constants for MCP Source Connector */ @@ -430,4 +430,4 @@ private McpSourceConstants() { * Health check endpoint suffix */ public static final String ENDPOINT_HEALTH = "/health"; -} \ No newline at end of file +} From 71e78be49ee6492018fb8d4f891dedb605f3d990 Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 23:02:42 +0800 Subject: [PATCH 32/36] Update McpStandardProtocol.java --- .../protocol/impl/McpStandardProtocol.java | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java index a092a0981b..737dae82de 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/protocol/impl/McpStandardProtocol.java @@ -17,28 +17,24 @@ package org.apache.eventmesh.connector.mcp.source.protocol.impl; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.Route; -import io.vertx.ext.web.handler.BodyHandler; -import lombok.extern.slf4j.Slf4j; import org.apache.eventmesh.common.Constants; import org.apache.eventmesh.common.config.connector.mcp.SourceConnectorConfig; -import org.apache.eventmesh.common.remote.offset.RecordOffset; -import org.apache.eventmesh.common.remote.offset.RecordPartition; import org.apache.eventmesh.connector.mcp.source.data.McpRequest; import org.apache.eventmesh.connector.mcp.source.data.McpResponse; import org.apache.eventmesh.connector.mcp.source.protocol.Protocol; import org.apache.eventmesh.openconnect.offsetmgmt.api.data.ConnectRecord; -import java.nio.charset.StandardCharsets; import java.util.Base64; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; import java.util.concurrent.BlockingQueue; -import java.util.stream.Collectors; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Route; +import io.vertx.ext.web.handler.BodyHandler; + +import lombok.extern.slf4j.Slf4j; /** * MCP Standard Protocol Implementation @@ -340,4 +336,4 @@ private void handleBase64Decoding(ConnectRecord connectRecord) { private String generateSessionId() { return "mcp-session-" + UUID.randomUUID(); } -} \ No newline at end of file +} From 113997d0447b6093fe739a2728b8330a103f1f63 Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 23:03:43 +0800 Subject: [PATCH 33/36] Update McpRequest.java --- .../eventmesh/connector/mcp/source/data/McpRequest.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java index 62be7849e3..38b9be8b53 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpRequest.java @@ -17,17 +17,16 @@ package org.apache.eventmesh.connector.mcp.source.data; +import java.io.Serializable; + import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; - /** * MCP Protocol Request * Represents a request in the MCP (Model Context Protocol) format @@ -89,4 +88,4 @@ public class McpRequest implements Serializable { * Vert.x routing context for HTTP response handling */ private transient RoutingContext routingContext; -} \ No newline at end of file +} From 99f9af295a95cbfce5da493949c5f2917b517e94 Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 23:04:22 +0800 Subject: [PATCH 34/36] Update McpResponse.java --- .../eventmesh/connector/mcp/source/data/McpResponse.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java index b439c6cd50..93e1cbcfab 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/source/data/McpResponse.java @@ -17,15 +17,16 @@ package org.apache.eventmesh.connector.mcp.source.data; +import java.io.Serializable; +import java.time.LocalDateTime; + import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONWriter.Feature; + import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import java.io.Serializable; -import java.time.LocalDateTime; - /** * MCP Response * Represents a response message for MCP protocol operations @@ -132,4 +133,4 @@ public static McpResponse error(String msg, Integer errorCode) { public static McpResponse base(String msg) { return new McpResponse("info", msg, LocalDateTime.now(), null, null); } -} \ No newline at end of file +} From 2554a2d8aa2d230100b05efdf9a08ecc4cbf0e80 Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 23:05:48 +0800 Subject: [PATCH 35/36] Update McpSinkHandlerRetryWrapper.java --- .../mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java index 52a935fbe6..c2e990857e 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/impl/McpSinkHandlerRetryWrapper.java @@ -30,13 +30,15 @@ import java.time.Duration; import java.util.Map; -import dev.failsafe.Failsafe; -import dev.failsafe.RetryPolicy; import io.vertx.core.Future; import io.vertx.core.buffer.Buffer; import io.vertx.ext.web.client.HttpResponse; + import lombok.extern.slf4j.Slf4j; +import dev.failsafe.Failsafe; +import dev.failsafe.RetryPolicy; + /** * McpSinkHandlerRetryWrapper is a wrapper class for the McpSinkHandler that provides retry functionality for failed Mcp requests. */ From 142224abfbe87607f490c595a98f8c8181ea0e62 Mon Sep 17 00:00:00 2001 From: wqliang Date: Fri, 31 Oct 2025 23:06:40 +0800 Subject: [PATCH 36/36] Update AbstractMcpSinkHandler.java --- .../connector/mcp/sink/handler/AbstractMcpSinkHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java index 0a0fea3be5..5c7435d037 100644 --- a/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java +++ b/eventmesh-connectors/eventmesh-connector-mcp/src/main/java/org/apache/eventmesh/connector/mcp/sink/handler/AbstractMcpSinkHandler.java @@ -17,7 +17,6 @@ package org.apache.eventmesh.connector.mcp.sink.handler; -import lombok.Getter; import org.apache.eventmesh.common.config.connector.mcp.SinkConnectorConfig; import org.apache.eventmesh.connector.mcp.sink.data.McpAttemptEvent; import org.apache.eventmesh.connector.mcp.sink.data.McpConnectRecord; @@ -31,6 +30,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import lombok.Getter; + public abstract class AbstractMcpSinkHandler implements McpSinkHandler { @Getter private final SinkConnectorConfig sinkConnectorConfig;