You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@rocketmq.apache.org by li...@apache.org on 2026/09/15 02:58:17 UTC

(rocketmq-dashboard) branch rocketmq-studio updated: feat(server): add MCP tool framework and AI integration. (#4225)

This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 6d29cb737 feat(server): add MCP tool framework and AI integration. (#4225)
6d29cb737 is described below

commit 6d29cb737be542505a6b1a7c652d32a6e55e4124
Author: echooymxq <ec...@gmail.com>
AuthorDate: Tue Sep 15 10:58:08 2026 +0800

    feat(server): add MCP tool framework and AI integration. (#4225)
---
 server/pom.xml                                     |  42 +-
 .../rocketmq/studio/auth/AuthInterceptor.java      |  53 +-
 .../apache/rocketmq/studio/auth/AuthWebConfig.java |  24 +-
 .../cluster/broker/BrokerConfigDiffService.java    |   9 +
 .../studio/cluster/broker/ClusterProvider.java     |  12 +-
 .../studio/cluster/broker/ClusterService.java      |  69 +-
 .../cluster/broker/RuntimeAdminClientResolver.java |  42 +-
 .../studio/cluster/client/ClientConnectionVO.java  |   1 +
 .../studio/cluster/metrics/MetricsService.java     |  18 +
 .../nameserver/NameServerConfigDiffService.java    |   8 +
 .../studio/cluster/proxy/ProxyAddressService.java  |  38 +
 .../config/LegacyJackson2Config.java}              |  37 +-
 .../studio/common/exception/BusinessException.java |   5 +
 .../rocketmq/studio/instance/InstanceResolver.java |  59 ++
 .../studio/instance/acl/AclRepository.java         |   2 +
 .../rocketmq/studio/instance/acl/AclService.java   |  34 +-
 .../instance/acl/MybatisPlusAclRepository.java     |   6 +
 .../rocketmq/studio/instance/dlq/DLQProvider.java  |   2 +-
 .../studio/instance/topic/MetadataService.java     | 214 ++++-
 .../rocketmq/studio/ops/ai/AiController.java       |  33 +-
 .../apache/rocketmq/studio/ops/ai/AiService.java   |  33 -
 .../ops/ai/{AiToolVO.java => AiToolCallDTO.java}   |  14 +-
 .../apache/rocketmq/studio/ops/ai/AiToolVO.java    |   4 +
 .../rocketmq/studio/ops/ai/McpToolController.java  |  64 ++
 .../ai/{AiController.java => ToolController.java}  |  50 +-
 .../studio/ops/ai/ToolExceptionHandler.java        |  75 ++
 .../ai/auth/McpAuthentication.java}                |  28 +-
 .../ai/auth/McpAuthenticationException.java}       |  15 +-
 .../ops/ai/auth/McpAuthenticationFilter.java       |  84 ++
 .../studio/ops/ai/auth/McpAuthenticator.java       | 222 ++++++
 .../studio/ops/ai/mcp/McpConfiguration.java        | 101 +++
 .../studio/ops/ai/mcp/McpToolRegistrar.java        | 141 ++++
 .../ops/ai/tool/AlertRuleListToolHandler.java      |  82 --
 .../ops/ai/tool/CapabilitiesToolHandler.java       |  64 --
 .../studio/ops/ai/tool/CapabilityResolver.java     |  64 --
 .../studio/ops/ai/tool/ClusterListToolHandler.java |  73 --
 .../ops/ai/tool/ConsumerGroupListToolHandler.java  |  92 ---
 .../ops/ai/tool/DashboardSummaryToolHandler.java   | 111 ---
 .../ops/ai/tool/MessageQueryToolHandler.java       | 127 ---
 .../ops/ai/tool/MessageTraceToolHandler.java       |  89 ---
 .../studio/ops/ai/tool/ToolAccessPolicy.java       |  86 --
 .../studio/ops/ai/tool/ToolDefinition.java         |  83 --
 .../studio/ops/ai/tool/ToolGatewayService.java     | 252 ------
 .../studio/ops/ai/tool/ToolListPagination.java     |  61 --
 .../studio/ops/ai/tool/TopicListToolHandler.java   |  85 --
 .../ops/ai/tool/catalog/CapabilityResolver.java    |  72 ++
 .../ops/ai/tool/{ => catalog}/ToolCatalog.java     | 130 +--
 .../ai/tool/contract/acl/AclListInput.java}        |  22 +-
 .../ops/ai/tool/contract/acl/AclMutationInput.java |  62 ++
 .../ops/ai/tool/contract/acl/AclRuleItem.java      |  48 ++
 .../ai/tool/contract/acl/AclUserItem.java}         |  33 +-
 .../ai/tool/contract/acl/UserCreateInput.java}     |  17 +-
 .../tool/contract/alert/AlertRuleListInput.java}   |  13 +-
 .../ai/tool/contract/alert/AlertRuleListItem.java  |  54 ++
 .../tool/contract/broker/BrokerConfigOutput.java   |  98 +++
 .../contract/broker/BrokerConfigUpdateInput.java}  |  22 +-
 .../contract/broker/BrokerConfigUpdateOutput.java  |  49 ++
 .../tool/contract/broker/BrokerDescribeInput.java} |  13 +-
 .../tool/contract/client/ClientDescribeInput.java} |  16 +-
 .../ai/tool/contract/client/ClientInstance.java    |  44 ++
 .../ai/tool/contract/client/ClientListInput.java}  |  13 +-
 .../tool/contract/cluster/ClusterListInput.java}   |  13 +-
 .../ai/tool/contract/cluster/ClusterListItem.java} |  43 +-
 .../tool/contract/common/BrokerClusterInput.java}  |  13 +-
 .../tool/contract/common/CapabilitiesOutput.java}  |  15 +-
 .../ai/tool/contract/common/ClusterInput.java}     |  13 +-
 .../tool/contract/common/ClusterListOutput.java}   |  13 +-
 .../ai/tool/contract/common/ListOutput.java}       |  13 +-
 .../ai/tool/contract/common/MutationOutput.java}   |  22 +-
 .../ai/tool/contract/common/PageOutput.java}       |  34 +-
 .../ai/tool/contract/common/PageRequest.java}      |  18 +-
 .../tool/contract/common/ResourceDeleteInput.java} |  13 +-
 .../ai/tool/contract/common/TimeRange.java}        |  13 +-
 .../ai/tool/contract/dlq/DLQClearInput.java}       |  15 +-
 .../ai/tool/contract/dlq/DLQListInput.java}        |  19 +-
 .../ops/ai/tool/contract/dlq/DLQListOutput.java    |  95 +++
 .../ai/tool/contract/dlq/DLQResendInput.java}      |  17 +-
 .../ai/tool/contract/dlq/DLQResendOutput.java}     |  41 +-
 .../ai/tool/contract/group/GroupClientsOutput.java |  63 ++
 .../tool/contract/group/GroupDescribeInput.java}   |  13 +-
 .../tool/contract/group/GroupDescribeOutput.java   | 117 +++
 .../ops/ai/tool/contract/group/GroupInput.java     |  78 ++
 .../ai/tool/contract/group/GroupListInput.java}    |  13 +-
 .../ops/ai/tool/contract/group/GroupListItem.java  |  50 ++
 .../tool/contract/group/GroupProgressOutput.java   |  73 ++
 .../contract/group/GroupResetOffsetInput.java}     |  13 +-
 .../ai/tool/contract/group/GroupTopicInput.java}   |  13 +-
 .../ai/tool/contract/group/ResetOffsetOutput.java} |  13 +-
 .../contract/group/SkipAccumulatedOutput.java}     |  15 +-
 .../ai/tool/contract/message/DlqMessageItem.java}  |  45 +-
 .../ops/ai/tool/contract/message/MessageItem.java  |  50 ++
 .../contract/message/MessageQueryByIdInput.java}   |  13 +-
 .../contract/message/MessageQueryByKeyInput.java}  |  18 +-
 .../message/MessageQueryByTopicInput.java}         |  18 +-
 .../tool/contract/message/MessageResendInput.java} |  19 +-
 .../contract/message/MessageResendOutput.java}     |  19 +-
 .../tool/contract/message/MessageTraceOutput.java} |  34 +-
 .../studio/ops/ai/tool/contract/ops/AuditItem.java |  46 ++
 .../ai/tool/contract/ops/AuditListInput.java}      |  23 +-
 .../ai/tool/contract/ops/MetricsQueryInput.java}   |  20 +-
 .../ai/tool/contract/plan/PlanDescription.java}    |  26 +-
 .../studio/ops/ai/tool/contract/plan/ToolPlan.java | 136 ++++
 .../contract/proxy/ProxyConfigUpdateInput.java}    |  13 +-
 .../contract/proxy/ProxyConfigUpdateOutput.java}   |  16 +-
 .../tool/contract/topic/LiteTopicListInput.java}   |  13 +-
 .../tool/contract/topic/TopicDescribeInput.java}   |  13 +-
 .../tool/contract/topic/TopicDescribeOutput.java   | 126 +++
 .../ops/ai/tool/contract/topic/TopicInput.java     |  62 ++
 .../ai/tool/contract/topic/TopicListInput.java}    |  13 +-
 .../ops/ai/tool/contract/topic/TopicListItem.java  |  50 ++
 .../ops/ai/tool/contract/topic/TopicOutput.java    |  44 ++
 .../ai/tool/contract/topic/TopicRouteInput.java}   |  13 +-
 .../ops/ai/tool/contract/topic/TopicRouteItem.java |  57 ++
 .../ai/tool/contract/topic/TopicSendInput.java}    |  19 +-
 .../ai/tool/contract/topic/TopicSendOutput.java}   |  21 +-
 .../ai/tool/contract/topic/TopicUpdateInput.java   |  59 ++
 .../ai/tool/core/ToolDefinition.java}              |  38 +-
 .../studio/ops/ai/tool/core/ToolError.java         | 198 +++++
 .../ops/ai/tool/core/ToolExecutionContext.java     | 129 +++
 .../ops/ai/tool/core/ToolExecutionException.java   |  62 ++
 .../ai/tool/core/ToolExecutionResult.java}         |  32 +-
 .../ai/tool/core/ToolHandler.java}                 |  15 +-
 .../studio/ops/ai/tool/core/ToolInvocation.java    |  49 ++
 .../ai/tool/core/ToolRiskLevel.java}               |  22 +-
 .../studio/ops/ai/tool/filter/ToolAuditFilter.java |  60 ++
 .../ops/ai/tool/filter/ToolCapabilityFilter.java   |  54 ++
 .../ai/tool/filter/ToolExecutionFilter.java}       |  24 +-
 .../ToolFilterChain.java}                          |  38 +-
 .../ops/ai/tool/filter/ToolMutationFilter.java     |  89 +++
 .../ToolValidationFilter.java}                     |  27 +-
 .../CapabilitiesToolHandler.java}                  |  30 +-
 .../ai/tool/handler/MutationToolHandler.java}      |  27 +-
 .../ai/tool/handler/acl/AclCreateToolHandler.java  |  62 ++
 .../ai/tool/handler/acl/AclDeleteToolHandler.java  |  64 ++
 .../ai/tool/handler/acl/AclListToolHandler.java    |  54 ++
 .../ai/tool/handler/acl/AclUpdateToolHandler.java  |  79 ++
 .../ai/tool/handler/acl/UserCreateToolHandler.java |  74 ++
 .../ai/tool/handler/acl/UserDeleteToolHandler.java |  64 ++
 .../acl/UserListToolHandler.java}                  |  32 +-
 .../handler/alert/AlertRuleListToolHandler.java    |  76 ++
 .../handler/broker/BrokerConfigToolHandler.java    |  50 ++
 .../broker/BrokerConfigUpdateToolHandler.java      |  84 ++
 .../handler/broker/BrokerDescribeToolHandler.java  |  53 ++
 .../tool/handler/broker/BrokerListToolHandler.java |  53 ++
 .../broker/BrokerRuntimeInfoToolHandler.java}      |  44 +-
 .../handler/client/ClientDescribeToolHandler.java  |  64 ++
 .../tool/handler/client/ClientListToolHandler.java |  61 ++
 .../handler/cluster/ClusterListToolHandler.java    |  61 ++
 .../dashboard/DashboardSummaryToolHandler.java     | 125 +++
 .../ai/tool/handler/dlq/DLQClearToolHandler.java   |  84 ++
 .../ai/tool/handler/dlq/DLQListToolHandler.java    |  87 +++
 .../ai/tool/handler/dlq/DLQResendToolHandler.java  |  80 ++
 .../group/ConsumerGroupCreateToolHandler.java      |  68 ++
 .../group/ConsumerGroupDeleteToolHandler.java      |  65 ++
 .../group/ConsumerGroupListToolHandler.java        |  57 ++
 .../group/ConsumerGroupUpdateToolHandler.java      |  72 ++
 .../handler/group/GroupClientsToolHandler.java     |  53 ++
 .../handler/group/GroupDescribeToolHandler.java    |  83 ++
 .../handler/group/GroupProgressToolHandler.java    |  53 ++
 .../handler/group/GroupResetOffsetToolHandler.java | 104 +++
 .../group/GroupSkipAccumulatedToolHandler.java     |  78 ++
 .../litetopic/LiteTopicCreateToolHandler.java      |  75 ++
 .../litetopic/LiteTopicListToolHandler.java        |  50 ++
 .../message/MessageQueryByIdToolHandler.java       |  58 ++
 .../message/MessageQueryByKeyToolHandler.java      |  59 ++
 .../message/MessageQueryByTopicToolHandler.java    |  59 ++
 .../handler/message/MessageResendToolHandler.java  |  88 +++
 .../handler/message/MessageTraceToolHandler.java   |  83 ++
 .../handler/metrics/MetricsQueryToolHandler.java   |  80 ++
 .../NameServerConfigDiffToolHandler.java           |  23 +-
 .../nameserver/NameserverListToolHandler.java      |  72 ++
 .../proxy/ProxyConfigUpdateToolHandler.java        |  77 ++
 .../handler/proxy/ProxyDescribeToolHandler.java    |  53 ++
 .../tool/handler/proxy/ProxyListToolHandler.java   |  53 ++
 .../tool/handler/topic/TopicCreateToolHandler.java |  69 ++
 .../tool/handler/topic/TopicDeleteToolHandler.java |  66 ++
 .../handler/topic/TopicDescribeToolHandler.java    |  60 ++
 .../tool/handler/topic/TopicListToolHandler.java   |  57 ++
 .../tool/handler/topic/TopicRouteToolHandler.java  |  57 ++
 .../tool/handler/topic/TopicSendToolHandler.java   |  97 +++
 .../tool/handler/topic/TopicUpdateToolHandler.java |  74 ++
 .../ops/ai/tool/service/ToolDiscoveryService.java  |  82 ++
 .../ops/ai/tool/service/ToolExecutionService.java  | 123 +++
 .../ops/ai/tool/service/ToolSchemaValidator.java   | 122 +++
 .../ops/ai/tool/service/ToolTokenService.java      | 220 ++++++
 .../rocketmq/studio/ops/alert/AlertRepository.java |   2 +
 .../rocketmq/studio/ops/alert/AlertService.java    |   7 +
 .../ops/alert/MybatisPlusAlertRepository.java      |   7 +
 .../rocketmq/studio/provider/InstanceProvider.java |   4 +
 .../studio/provider/InstanceProviderRegistry.java  |  10 +-
 .../studio/provider/apache/AdminClient.java        |  13 +-
 .../provider/apache/ApacheInstanceProvider.java    |   9 +-
 .../provider/apache/RocketMQAdminClientImpl.java   | 230 ++++--
 .../provider/apache/RocketMQClusterProvider.java   |  29 +-
 .../apache/RocketMQDefaultClusterResolver.java     |  86 ++
 .../provider/apache/RocketMQMetadataProvider.java  |  24 +-
 server/src/main/resources/application.yml          |  56 +-
 .../src/main/resources/tool-catalog/manifest.yaml  |   2 +
 .../resources/tool-catalog/rmq-tools.schema.json   |   8 +-
 .../src/main/resources/tool-catalog/rmq-tools.yaml | 653 ----------------
 .../src/main/resources/tool-catalog/tools/acl.yaml | 351 +++++++++
 .../main/resources/tool-catalog/tools/alert.yaml   |  69 ++
 .../main/resources/tool-catalog/tools/broker.yaml  | 396 ++++++++++
 .../resources/tool-catalog/tools/capabilities.yaml |  36 +
 .../main/resources/tool-catalog/tools/client.yaml  | 182 +++++
 .../main/resources/tool-catalog/tools/cluster.yaml |  61 ++
 .../resources/tool-catalog/tools/dashboard.yaml    | 126 +++
 .../src/main/resources/tool-catalog/tools/dlq.yaml | 287 +++++++
 .../main/resources/tool-catalog/tools/group.yaml   | 870 +++++++++++++++++++++
 .../resources/tool-catalog/tools/lite_topic.yaml   | 179 +++++
 .../main/resources/tool-catalog/tools/message.yaml | 371 +++++++++
 .../main/resources/tool-catalog/tools/metrics.yaml |  94 +++
 .../resources/tool-catalog/tools/nameserver.yaml   | 155 ++++
 .../main/resources/tool-catalog/tools/proxy.yaml   | 198 +++++
 .../main/resources/tool-catalog/tools/topic.yaml   | 667 ++++++++++++++++
 .../main/resources/tool-catalog/tools/user.yaml    | 229 ++++++
 .../studio/HealthProbeIntegrationTest.java         |   2 +-
 .../rocketmq/studio/StudioApplicationTest.java     |  76 +-
 .../rocketmq/studio/WebMvcAuthTestSupport.java     |  48 ++
 .../rocketmq/studio/auth/AuthControllerTest.java   |  19 +-
 .../studio/auth/AuthCorsIntegrationTest.java       |  23 +-
 ...AuthCredentialAuthorizationIntegrationTest.java |  25 +-
 .../rocketmq/studio/auth/AuthInterceptorTest.java  |  17 +-
 .../studio/auth/StudioUserControllerTest.java      |  20 +-
 .../broker/BrokerConfigDiffServiceTest.java        |  15 +
 .../cluster/broker/ClusterControllerTest.java      |  20 +-
 .../studio/cluster/broker/ClusterServiceTest.java  |  77 +-
 .../broker/RuntimeAdminClientResolverTest.java     |  82 +-
 .../cluster/client/ClientControllerTest.java       |  16 +-
 .../cluster/client/ProducerControllerTest.java     |  16 +-
 .../studio/cluster/k8s/K8sCertControllerTest.java  |  16 +-
 .../cluster/metrics/MetricsControllerTest.java     |  18 +-
 .../grafana/GrafanaDashboardControllerTest.java    |  16 +-
 .../NameServerConfigDiffServiceTest.java           |  20 +
 .../nameserver/NameServerControllerTest.java       |  20 +-
 .../cluster/proxy/ProxyAddressServiceTest.java     |  42 +
 .../cluster/proxy/ProxyCompatControllerTest.java   |  16 +-
 .../studio/cluster/proxy/ProxyControllerTest.java  |  18 +-
 .../studio/common/config/CorsConfigTest.java       |   8 +-
 .../exception/GlobalExceptionHandlerTest.java      |   2 +-
 .../studio/instance/InstanceControllerTest.java    |  18 +-
 .../studio/instance/InstanceResolverTest.java      | 102 +++
 .../studio/instance/acl/AclControllerTest.java     |  18 +-
 .../studio/instance/acl/AclServiceTest.java        |  15 +-
 .../studio/instance/dlq/DLQControllerTest.java     |  16 +-
 .../group/ConsumerGroupControllerTest.java         |  20 +-
 .../instance/message/MessageControllerTest.java    |  16 +-
 .../message/QueryHistoryControllerTest.java        |  16 +-
 .../instance/topic/LiteTopicControllerTest.java    |  16 +-
 .../studio/instance/topic/MetadataServiceTest.java | 158 +++-
 .../studio/instance/topic/TopicControllerTest.java |  18 +-
 .../rocketmq/studio/ops/OpsControllerTest.java     |  16 +-
 .../rocketmq/studio/ops/ai/AiControllerTest.java   | 113 +--
 .../rocketmq/studio/ops/ai/AiServiceTest.java      |  87 +--
 .../rocketmq/studio/ops/ai/LlmControllerTest.java  |  15 +-
 .../rocketmq/studio/ops/ai/ToolControllerTest.java | 168 ++++
 .../ai/auth/McpCredentialAuthenticationTest.java   | 374 +++++++++
 .../studio/ops/ai/mcp/McpConfigurationTest.java    |  71 ++
 .../studio/ops/ai/mcp/McpToolRegistrarTest.java    | 105 +++
 .../ops/ai/tool/CapabilitiesToolHandlerTest.java   |  51 +-
 .../studio/ops/ai/tool/CapabilityResolverTest.java |  92 +++
 .../ops/ai/tool/ClusterListToolHandlerTest.java    |  78 +-
 .../ai/tool/ConsumerGroupListToolHandlerTest.java  |  75 --
 .../ops/ai/tool/MessageQueryToolHandlerTest.java   | 133 ----
 .../ops/ai/tool/MessageTraceToolHandlerTest.java   |  33 +-
 .../ops/ai/tool/TestToolExecutionContexts.java     |  26 +
 .../studio/ops/ai/tool/ToolCatalogTest.java        | 167 ----
 .../studio/ops/ai/tool/ToolGatewayServiceTest.java | 865 --------------------
 .../ops/ai/tool/TopicListToolHandlerTest.java      |  78 --
 .../ops/ai/tool/catalog/ToolCatalogTest.java       | 117 +++
 .../ai/tool/catalog/ToolCatalogTestSupport.java    |  45 ++
 .../ops/ai/tool/filter/ToolMutationFilterTest.java | 169 ++++
 .../tool/handler/acl/AclCreateToolHandlerTest.java |  97 +++
 .../tool/handler/acl/AclDeleteToolHandlerTest.java |  50 ++
 .../tool/handler/acl/AclListToolHandlerTest.java   |  82 ++
 .../ai/tool/handler/acl/AclMutationPlanTest.java   |  81 ++
 .../tool/handler/acl/AclUpdateToolHandlerTest.java |  70 ++
 .../broker/BrokerConfigUpdateToolHandlerTest.java  |  78 ++
 .../handler/broker/BrokerListToolHandlerTest.java  |  70 ++
 .../broker/NameserverListToolHandlerTest.java      |  59 ++
 .../client/ClientDescribeToolHandlerTest.java      |  71 ++
 .../handler/client/ClientListToolHandlerTest.java  |  86 ++
 .../dashboard/DashboardSummaryToolHandlerTest.java |  40 +
 .../tool/handler/dlq/DLQClearToolHandlerTest.java  |  44 ++
 .../tool/handler/dlq/DLQListToolHandlerTest.java   | 121 +++
 .../tool/handler/dlq/DLQResendToolHandlerTest.java | 120 +++
 .../group/ConsumerGroupReadToolHandlersTest.java   | 147 ++++
 .../tool/handler/group/GroupMutationPlanTest.java  | 162 ++++
 .../group/GroupResetOffsetToolHandlerTest.java     |  50 ++
 .../message/MessageQueryByIdToolHandlerTest.java   |  80 ++
 .../message/MessageQueryByKeyToolHandlerTest.java  |  72 ++
 .../MessageQueryByTopicToolHandlerTest.java        |  82 ++
 .../message/MessageResendToolHandlerTest.java      |  88 +++
 .../metrics/MetricsQueryToolHandlerTest.java       |  92 +++
 .../proxy/ProxyConfigUpdateToolHandlerTest.java    |  50 ++
 .../handler/proxy/ProxyListToolHandlerTest.java    |  66 ++
 .../tool/handler/topic/TopicMutationPlanTest.java  | 156 ++++
 .../ai/tool/service/ToolDiscoveryServiceTest.java  | 133 ++++
 .../tool/service/ToolExecutorInvocationTest.java   | 175 +++++
 .../ai/tool/service/ToolInstanceRoutingTest.java   | 111 +++
 .../tool/service/ToolOutputSchemaContractTest.java |  88 +++
 .../tool/service/ToolTokenConfigurationTest.java   | 168 ++++
 .../ops/ai/tool/service/ToolTokenServiceTest.java  | 132 ++++
 .../ops/alert/AlertRuleAssetControllerTest.java    |  16 +-
 .../studio/ops/alert/AlertRuleControllerTest.java  |  22 +-
 .../studio/ops/alert/AlertSchemaMigrationTest.java |   8 +
 .../ops/alert/AlertSilenceControllerTest.java      |   8 +-
 .../ops/alert/ClusterAlertRuleControllerTest.java  |  22 +-
 .../NativeAlertEvaluationTransactionTest.java      |  16 +-
 .../ops/alert/SystemAlertControllerTest.java       |  18 +-
 .../studio/ops/audit/AuditControllerTest.java      |  16 +-
 .../studio/ops/audit/AuditServiceTest.java         |   4 +-
 .../ops/dashboard/DashboardControllerTest.java     |  16 +-
 .../provider/InstanceProviderRegistryTest.java     |  21 +-
 .../apache/ApacheInstanceProviderTest.java         |  20 +
 .../apache/RocketMQAdminClientImplTest.java        | 285 ++++++-
 .../apache/RocketMQBrokerConfigServiceTest.java    |  12 +-
 .../apache/RocketMQClusterProviderTest.java        |  26 +-
 .../apache/RocketMQClusterResolverTest.java        |  85 ++
 .../apache/RocketMQMetadataProviderTest.java       |  21 +
 .../credential/CloudCredentialControllerTest.java  |  15 +-
 .../studio/settings/SettingsControllerTest.java    |  16 +-
 322 files changed, 18034 insertions(+), 4974 deletions(-)

diff --git a/server/pom.xml b/server/pom.xml
index 10855c3cc..b057278f8 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -7,7 +7,7 @@
     <parent>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-parent</artifactId>
-        <version>3.5.3</version>
+        <version>4.1.0</version>
         <relativePath/>
     </parent>
 
@@ -18,15 +18,36 @@
 
     <properties>
         <java.version>21</java.version>
+        <spring-ai.version>2.0.0</spring-ai.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <rocketmq.version>5.5.0</rocketmq.version>
     </properties>
 
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.ai</groupId>
+                <artifactId>spring-ai-bom</artifactId>
+                <version>${spring-ai.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
     <dependencies>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-restclient</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-mail</artifactId>
@@ -42,12 +63,12 @@
         <dependency>
             <groupId>com.networknt</groupId>
             <artifactId>json-schema-validator</artifactId>
-            <version>2.0.4</version>
+            <version>3.0.0</version>
         </dependency>
         <dependency>
             <groupId>org.springdoc</groupId>
             <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
-            <version>2.8.5</version>
+            <version>3.0.3</version>
         </dependency>
         <dependency>
             <groupId>org.projectlombok</groupId>
@@ -60,6 +81,11 @@
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webmvc-test</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>com.tngtech.archunit</groupId>
             <artifactId>archunit-junit5</artifactId>
@@ -73,8 +99,13 @@
         <!-- MyBatis-Plus + MySQL -->
         <dependency>
             <groupId>com.baomidou</groupId>
-            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
-            <version>3.5.7</version>
+            <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
+            <version>3.5.17</version>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-jsqlparser</artifactId>
+            <version>3.5.17</version>
         </dependency>
         <dependency>
             <groupId>com.mysql</groupId>
@@ -84,7 +115,6 @@
         <dependency>
             <groupId>com.h2database</groupId>
             <artifactId>h2</artifactId>
-            <scope>test</scope>
         </dependency>
         <!-- RocketMQ Admin Tools (kept from trunk #694) -->
         <dependency>
diff --git a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
index 8e430788b..5be65010b 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
@@ -19,7 +19,8 @@ package org.apache.rocketmq.studio.auth;
 
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
-import org.apache.rocketmq.studio.ops.ai.tool.ToolAccessPolicy;
+import org.apache.rocketmq.studio.ops.ai.tool.catalog.ToolCatalog;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolDefinition;
 import org.apache.rocketmq.studio.settings.GeneralSettingsVO;
 import org.apache.rocketmq.studio.settings.SettingsRepository;
 import org.springframework.http.HttpMethod;
@@ -28,10 +29,14 @@ import org.springframework.http.MediaType;
 import org.springframework.web.cors.CorsUtils;
 import org.springframework.web.servlet.HandlerInterceptor;
 
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
 import java.util.Set;
 
 public class AuthInterceptor implements HandlerInterceptor {
 
+    private static final String TOOL_EXECUTION_PREFIX = "/api/ai/tools/";
+    private static final String TOOL_EXECUTION_SUFFIX = "/execute";
     private static final Set<String> READER_POST_PATHS = Set.of(
             "/api/auth/logout",
             "/api/auth/password",
@@ -42,19 +47,14 @@ public class AuthInterceptor implements HandlerInterceptor {
     private final AuthProperties authProperties;
     private final AuthService authService;
     private final SettingsRepository settingsRepository;
-    private final ToolAccessPolicy toolAccessPolicy;
+    private final ToolCatalog toolCatalog;
 
     public AuthInterceptor(AuthProperties authProperties, AuthService authService,
-                           SettingsRepository settingsRepository) {
-        this(authProperties, authService, settingsRepository, null);
-    }
-
-    public AuthInterceptor(AuthProperties authProperties, AuthService authService,
-                           SettingsRepository settingsRepository, ToolAccessPolicy toolAccessPolicy) {
+                           SettingsRepository settingsRepository, ToolCatalog toolCatalog) {
         this.authProperties = authProperties;
         this.authService = authService;
         this.settingsRepository = settingsRepository;
-        this.toolAccessPolicy = toolAccessPolicy;
+        this.toolCatalog = toolCatalog;
     }
 
     @Override
@@ -62,9 +62,7 @@ public class AuthInterceptor implements HandlerInterceptor {
                              Object handler) throws Exception {
         AuthenticatedUserContext.clear();
         if (!isLoginRequired() || CorsUtils.isPreFlightRequest(request)
-                || isPublicPath(requestPath(request)) || authService == null) {
-            // Slice tests and minimal contexts may not provide AuthService; fall back to no
-            // enforcement, matching the documented AuthWebConfig behaviour.
+                || isPublicPath(requestPath(request))) {
             return true;
         }
         String authorization = AuthCookie.authorization(request, authProperties);
@@ -92,10 +90,7 @@ public class AuthInterceptor implements HandlerInterceptor {
      * settings UI actually changes the enforced policy.
      */
     private boolean isLoginRequired() {
-        if (authProperties != null && authProperties.isLoginRequired()) {
-            return true;
-        }
-        if (settingsRepository == null) {
+        if (authProperties.isLoginRequired()) {
             return true;
         }
         try {
@@ -115,12 +110,34 @@ public class AuthInterceptor implements HandlerInterceptor {
             return isAdminOnlyGetPath(path);
         }
         String normalizedPath = normalizePath(stripPathParameters(path));
-        if (toolAccessPolicy != null && toolAccessPolicy.isToolExecutionPath(normalizedPath)) {
-            return !toolAccessPolicy.isReaderAccessiblePath(normalizedPath);
+        if (isToolExecutionPath(normalizedPath)) {
+            return !isReaderAccessibleToolPath(normalizedPath);
         }
         return !HttpMethod.POST.matches(method) || !READER_POST_PATHS.contains(normalizePath(path));
     }
 
+    private boolean isToolExecutionPath(String path) {
+        return path.startsWith(TOOL_EXECUTION_PREFIX)
+                && path.endsWith(TOOL_EXECUTION_SUFFIX);
+    }
+
+    private boolean isReaderAccessibleToolPath(String path) {
+        String encodedName = path.substring(
+                TOOL_EXECUTION_PREFIX.length(),
+                path.length() - TOOL_EXECUTION_SUFFIX.length());
+        if (encodedName.isBlank()) {
+            return false;
+        }
+        try {
+            String toolName = URLDecoder.decode(encodedName, StandardCharsets.UTF_8);
+            return toolCatalog.find(toolName)
+                    .map(ToolDefinition::isLowRiskReadOnly)
+                    .orElse(false);
+        } catch (IllegalArgumentException exception) {
+            return false;
+        }
+    }
+
     private boolean isAdminOnlyGetPath(String path) {
         String normalizedPath = normalizePath(stripPathParameters(path));
         return "/api/llm/config".equals(normalizedPath)
diff --git a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthWebConfig.java b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthWebConfig.java
index 17d813147..ca3f929ad 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthWebConfig.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthWebConfig.java
@@ -18,9 +18,8 @@
 package org.apache.rocketmq.studio.auth;
 
 import lombok.RequiredArgsConstructor;
-import org.apache.rocketmq.studio.ops.ai.tool.ToolAccessPolicy;
+import org.apache.rocketmq.studio.ops.ai.tool.catalog.ToolCatalog;
 import org.apache.rocketmq.studio.settings.SettingsRepository;
-import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -35,20 +34,17 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 @RequiredArgsConstructor
 public class AuthWebConfig implements WebMvcConfigurer {
 
-    private final ObjectProvider<AuthProperties> authPropertiesProvider;
-    private final ObjectProvider<AuthService> authServiceProvider;
-    private final ObjectProvider<SettingsRepository> settingsRepositoryProvider;
-    private final ObjectProvider<ToolAccessPolicy> toolAccessPolicyProvider;
+    private final AuthProperties authProperties;
+    private final AuthService authService;
+    private final SettingsRepository settingsRepository;
+    private final ToolCatalog toolCatalog;
 
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
-        // Slice tests and minimal contexts may not provide any of these beans; when they are
-        // missing the interceptor falls back to the static login-required property (effectively
-        // no enforcement), matching the old conditional-registration behaviour.
-        registry.addInterceptor(new AuthInterceptor(authPropertiesProvider.getIfAvailable(),
-                        authServiceProvider.getIfAvailable(),
-                        settingsRepositoryProvider.getIfAvailable(),
-                        toolAccessPolicyProvider.getIfAvailable()))
-                .addPathPatterns("/api/**");
+        registry.addInterceptor(new AuthInterceptor(
+                        authProperties, authService, settingsRepository,
+                        toolCatalog))
+                .addPathPatterns("/api/**")
+                .excludePathPatterns("/api/mcp/**");
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/BrokerConfigDiffService.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/BrokerConfigDiffService.java
index 771d37e56..e2587eaa4 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/BrokerConfigDiffService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/BrokerConfigDiffService.java
@@ -49,6 +49,11 @@ public class BrokerConfigDiffService {
     private final ClusterService clusterService;
     private final RocketMQBrokerConfigService brokerConfigService;
 
+    public BrokerConfigDiffVO compareForInstance(String instanceId) {
+        ClusterVO cluster = clusterService.requireSingleCluster(instanceId);
+        return compare(cluster.getId(), instanceId, collectBrokerTargets(cluster));
+    }
+
     public BrokerConfigDiffVO compare(String clusterId, String instanceId) {
         String normalizedClusterId = requireClusterId(clusterId);
         String normalizedInstanceId = normalizeInstanceId(instanceId);
@@ -56,6 +61,10 @@ public class BrokerConfigDiffService {
                 ? clusterService.getCluster(normalizedClusterId)
                 : clusterService.getCluster(normalizedClusterId, normalizedInstanceId);
         List<BrokerTarget> brokers = collectBrokerTargets(cluster);
+        return compare(normalizedClusterId, normalizedInstanceId, brokers);
+    }
+
+    private BrokerConfigDiffVO compare(String normalizedClusterId, String normalizedInstanceId, List<BrokerTarget> brokers) {
         if (brokers.isEmpty()) {
             throw new BusinessException(409, "Cluster has no broker endpoints: " + normalizedClusterId);
         }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
index 141b1cef2..df2bc5b6f 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
@@ -24,16 +24,24 @@ public interface ClusterProvider {
     List<ClusterVO> discoverClusters();
 
     default List<ClusterVO> discoverClusters(String instanceId) {
-        return discoverClusters();
+        return instanceId == null || instanceId.isBlank() ? discoverClusters() : List.of();
     }
 
     default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
         return List.of();
     }
 
+    default List<org.apache.rocketmq.studio.cluster.proxy.ProxyVO> discoverProxies(String instanceId) {
+        throw new UnsupportedOperationException("Instance Proxy discovery is not supported");
+    }
+
+    default List<BrokerVO> discoverBrokers(String instanceId, String brokerName) {
+        throw new UnsupportedOperationException("Instance Broker discovery is not supported");
+    }
+
     ClusterVO refreshClusterDetail(String clusterId);
 
     default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
-        return refreshClusterDetail(clusterId);
+        return instanceId == null || instanceId.isBlank() ? refreshClusterDetail(clusterId) : null;
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterService.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterService.java
index 9cb0d077f..19ccafa6f 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterService.java
@@ -130,6 +130,11 @@ public class ClusterService {
         return List.of();
     }
 
+    /**
+     * Verifies that a tool cluster target is confined to the authenticated Instance.
+     * A distinct cluster id must resolve through that Instance's provider and credentials;
+     * the global repository is deliberately not consulted at this security boundary.
+     */
     public ClusterVO getCluster(String id) {
         log.info("Getting cluster detail: {}", id);
         ClusterVO live = clusterProvider.refreshClusterDetail(id);
@@ -150,6 +155,13 @@ public class ClusterService {
         throw new BusinessException(503, "Cluster details are unavailable: " + id);
     }
 
+    public List<ProxyVO> listProxiesForInstance(String instanceId) {
+        if (instanceId == null || instanceId.isBlank()) {
+            throw new BusinessException(400, "Instance is required for proxy discovery");
+        }
+        return clusterProvider.discoverProxies(instanceId);
+    }
+
     public List<ProxyVO> listProxies(String clusterId) {
         ClusterVO cluster = resolveCluster(clusterId);
         if (cluster.getProxies() == null || cluster.getProxies().isEmpty()) {
@@ -238,9 +250,24 @@ public class ClusterService {
     }
 
     public ClusterConfigUpdateResultVO updateClusterConfig(UpdateConfigDTO command) {
+        return updateClusterConfig(command, null);
+    }
+
+    public ClusterConfigUpdateResultVO updateClusterConfig(UpdateConfigDTO command, String instanceId) {
+        if (instanceId != null) {
+            command.setInstanceId(instanceId);
+        }
+        return updateClusterConfigInternal(command, instanceId);
+    }
+
+    private ClusterConfigUpdateResultVO updateClusterConfigInternal(UpdateConfigDTO command, String instanceId) {
         log.info("Updating cluster config for: {}", command.getId());
         requireMatchingDefaultQueueNums(command);
-        ClusterVO cluster = resolveCluster(command.getId(), command.getInstanceId());
+        ClusterVO cluster = instanceId == null ? resolveCluster(command.getId(), command.getInstanceId())
+                : requireSingleCluster(instanceId);
+        if (instanceId != null) {
+            command.setId(cluster.getId());
+        }
 
         ClusterConfigVO config = copyConfig(cluster.getConfig());
         applyConfig(command, config);
@@ -292,6 +319,46 @@ public class ClusterService {
                 .build();
     }
 
+    public Map<String, ClusterConfigVO> readBrokerConfigs(String instanceId) {
+        Map<String, ClusterConfigVO> configs = new java.util.TreeMap<>();
+        ClusterVO cluster = requireSingleCluster(instanceId);
+        if (cluster.getBrokers() != null) {
+            for (BrokerVO broker : cluster.getBrokers()) {
+                configs.put(broker.getAddr(), brokerConfigService.getBrokerConfig(broker.getAddr(), instanceId));
+            }
+        }
+        return configs;
+    }
+
+    public Map<String, ClusterConfigVO> proposeBrokerConfigs(UpdateConfigDTO command, Map<String, ClusterConfigVO> current) {
+        requireMatchingDefaultQueueNums(command);
+        Map<String, ClusterConfigVO> proposed = new java.util.TreeMap<>();
+        current.forEach((address, config) -> {
+            ClusterConfigVO changed = copyConfig(config);
+            applyConfig(command, changed);
+            proposed.put(address, changed);
+        });
+        return proposed;
+    }
+
+    public ClusterVO requireSingleCluster(String instanceId) {
+        if (instanceId == null || instanceId.isBlank()) {
+            throw new BusinessException(400, "Instance is required for cluster discovery");
+        }
+        List<ClusterVO> clusters = clusterProvider.discoverClusters(instanceId);
+        if (clusters == null || clusters.isEmpty()) {
+            throw new BusinessException(503, "Cluster details are unavailable for instance: " + instanceId);
+        }
+        if (clusters.size() != 1) {
+            throw new BusinessException(409, "Instance exposes multiple Broker clusters: " + instanceId);
+        }
+        ClusterVO cluster = clusters.getFirst();
+        if (cluster.getId() == null || cluster.getId().isBlank()) {
+            throw new BusinessException(503, "Broker cluster name is unavailable for instance: " + instanceId);
+        }
+        return cluster;
+    }
+
     private void applyConfig(UpdateConfigDTO command, ClusterConfigVO config) {
         if (command.getFlushDiskType() != null) {
             config.setFlushDiskType(parseFlushDiskType(command.getFlushDiskType()));
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/RuntimeAdminClientResolver.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/RuntimeAdminClientResolver.java
index f689e3acd..7876db3f8 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/RuntimeAdminClientResolver.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/RuntimeAdminClientResolver.java
@@ -14,7 +14,7 @@ import org.apache.rocketmq.client.producer.DefaultMQProducer;
 import org.apache.rocketmq.remoting.RPCHook;
 import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
-import org.apache.rocketmq.studio.instance.InstanceRepository;
+import org.apache.rocketmq.studio.instance.InstanceResolver;
 import org.apache.rocketmq.studio.instance.InstanceVO;
 import org.springframework.stereotype.Component;
 import org.springframework.util.StringUtils;
@@ -22,17 +22,34 @@ import org.springframework.util.StringUtils;
 @Component
 @RequiredArgsConstructor
 public class RuntimeAdminClientResolver {
-
-    private final InstanceRepository instanceRepository;
+    private final InstanceResolver instanceResolver;
     private final MqAdminExtFactory adminFactory;
     private final MqAdminProperties adminProperties;
     private final MqClientPool clientPool;
 
+    public String configuredClusterName(String identifier) {
+        return instanceResolver.resolveClusterName(identifier);
+    }
+
+    public MqAdminProperties.Credential resolveCredential(InstanceVO instance) {
+        String credentialRef = credentialRef(instance);
+        if (!StringUtils.hasText(credentialRef)) {
+            throw new BusinessException(422, "Instance has no admin credential reference: " + instance.getName());
+        }
+        MqAdminProperties.Credential credential = adminProperties.getCredentials().get(credentialRef);
+        if (credential == null || !StringUtils.hasText(credential.getAccessKey())
+                || !StringUtils.hasText(credential.getSecretKey())) {
+            throw new BusinessException(422,
+                    "Admin credential reference is not configured: " + credentialRef);
+        }
+        return credential;
+    }
+
     public InstanceVO resolveInstance(String instanceId) {
         if (!StringUtils.hasText(instanceId)) {
             throw new BusinessException(400, "instanceId is required");
         }
-        return instanceRepository.findByIdentifier(instanceId)
+        return instanceResolver.findByIdentifier(instanceId)
                 .orElseThrow(() -> new BusinessException(404, "Instance not found: " + instanceId));
     }
 
@@ -44,15 +61,6 @@ public class RuntimeAdminClientResolver {
         return instance.getEndpoint().trim();
     }
 
-    /**
-     * Resolves the ACL hook for short-lived runtime clients that cannot be created by
-     * {@link MqAdminExtFactory}, such as a {@code DefaultMQPullConsumer}.
-     */
-    public RPCHook resolveCredentialHook(String instanceId) {
-        InstanceVO instance = requireApacheInstance(resolveInstance(instanceId));
-        return resolveCredential(credentialRef(instance));
-    }
-
     public <T> T execute(String instanceId, MqAdminExtFactory.AdminAction<T> action) {
         return execute(resolveInstance(instanceId), action);
     }
@@ -63,7 +71,7 @@ public class RuntimeAdminClientResolver {
             throw new BusinessException(400, "Instance endpoint is required");
         }
         String credentialRef = credentialRef(instance);
-        return adminFactory.execute(instance.getEndpoint().trim(), resolveCredential(credentialRef),
+        return adminFactory.execute(instance.getEndpoint().trim(), createCredentialHook(credentialRef),
                 credentialRef, action);
     }
 
@@ -72,7 +80,7 @@ public class RuntimeAdminClientResolver {
                                      MqClientPool.ClientAction<DefaultMQPullConsumer, T> action) {
         InstanceVO instance = requireApacheInstance(resolveInstance(instanceId));
         String credentialRef = credentialRef(instance);
-        return clientPool.withPullConsumer(requireEndpoint(instance), resolveCredential(credentialRef),
+        return clientPool.withPullConsumer(requireEndpoint(instance), createCredentialHook(credentialRef),
                 credentialRef, action);
     }
 
@@ -81,7 +89,7 @@ public class RuntimeAdminClientResolver {
                                  MqClientPool.ClientAction<DefaultMQProducer, T> action) {
         InstanceVO instance = requireApacheInstance(resolveInstance(instanceId));
         String credentialRef = credentialRef(instance);
-        return clientPool.withProducer(requireEndpoint(instance), resolveCredential(credentialRef),
+        return clientPool.withProducer(requireEndpoint(instance), createCredentialHook(credentialRef),
                 credentialRef, action);
     }
 
@@ -97,7 +105,7 @@ public class RuntimeAdminClientResolver {
                 ? instance.getAdminCredentialRef().trim() : null;
     }
 
-    private RPCHook resolveCredential(String credentialRef) {
+    private RPCHook createCredentialHook(String credentialRef) {
         if (!StringUtils.hasText(credentialRef)) {
             return null;
         }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ClientConnectionVO.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ClientConnectionVO.java
index 479dc2ce0..ea6140c13 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ClientConnectionVO.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/client/ClientConnectionVO.java
@@ -39,6 +39,7 @@ public class ClientConnectionVO {
     private String address;
     private ClientLanguage language;
     private String version;
+    @com.fasterxml.jackson.annotation.JsonFormat(shape = com.fasterxml.jackson.annotation.JsonFormat.Shape.STRING)
     private LocalDateTime connectedAt;
     private boolean partial;
     private String clusterName;
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricsService.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricsService.java
index 2f6b5206f..a50e4f771 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricsService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricsService.java
@@ -101,6 +101,24 @@ public class MetricsService {
         return source.query(resolvedQuery);
     }
 
+    /** Tool queries require a dedicated source explicitly bound to this Instance. */
+    public MetricDataVO queryInstance(String instanceId, MetricQueryDTO query) {
+        if (!StringUtils.hasText(instanceId)) throw badRequest("Instance is required for metrics query");
+        List<DataSourceVO> sources = settingsService.listDataSources().stream()
+                .filter(source -> source.getInstanceIds() != null && source.getInstanceIds().size() == 1
+                        && instanceId.equals(source.getInstanceIds().get(0))).toList();
+        if (sources.size() != 1) throw new org.apache.rocketmq.studio.common.exception.BusinessException(409,
+                "Configure exactly one dedicated metrics data source bound only to Instance " + instanceId);
+        DataSourceVO source = sources.get(0);
+        if (!"none".equals(normalizeAuth(source.getAuth())))
+            throw new org.apache.rocketmq.studio.common.exception.BusinessException(501,
+                    "Tool queries do not support data sources requiring per-request credentials");
+        MetricsDataSourceQueryRequest request = new MetricsDataSourceQueryRequest();
+        request.setInstanceId(instanceId);
+        request.setQuery(query);
+        return queryByDataSource(source.getKey(), request);
+    }
+
     private void validateInstanceBinding(String dataSourceKey, DataSourceVO dataSource, String instanceId) {
         List<String> bindings = dataSource.getInstanceIds();
         if (bindings == null || bindings.isEmpty()) {
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffService.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffService.java
index 7179ed8ac..cdcc6ccbe 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameServerConfigDiffService.java
@@ -75,6 +75,14 @@ public class NameServerConfigDiffService {
     private final MqAdminExtFactory adminFactory;
     private final RuntimeAdminClientResolver runtimeAdminClientResolver;
 
+    public NameServerConfigDiffVO compareForInstance(String instanceId) {
+        if (instanceId == null || instanceId.isBlank()) {
+            throw new BusinessException(400, "Instance is required for NameServer config comparison");
+        }
+        String endpoint = runtimeAdminClientResolver.resolveEndpoint(instanceId);
+        return compare(instanceId, ClusterVO.builder().endpoint(endpoint).build(), instanceId);
+    }
+
     public NameServerConfigDiffVO compare(String clusterId) {
         String normalizedClusterId = requireClusterId(clusterId);
         return compare(normalizedClusterId, clusterService.getCluster(normalizedClusterId), null);
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressService.java b/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressService.java
index 16a0a1f2b..90eac1e27 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressService.java
@@ -168,6 +168,10 @@ public class ProxyAddressService {
         synchronized (this) {
             addrs = new ArrayList<>(proxyAddrs);
         }
+        return buildTopology(addrs);
+    }
+
+    private List<ProxyTopologyVO> buildTopology(List<String> addrs) {
         List<ProbeTask> tasks = new ArrayList<>();
         for (String addr : addrs) {
             Matcher matcher = PROXY_ADDR_PATTERN.matcher(addr);
@@ -302,6 +306,40 @@ public class ProxyAddressService {
         String normalizedClusterId = normalizeClusterId(clusterId);
         String normalized = normalizeProxyAddr(addr, "addr");
         clusterService.requireProxy(normalizedClusterId, normalized);
+        reloadValidatedProxy(normalizedClusterId, normalized);
+    }
+
+    public ProxyTopologyVO previewReloadForInstance(String instanceId, String addr) {
+        ProxyVO proxy = requireInstanceProxy(instanceId, addr);
+        String normalized = normalizeProxyAddr(proxy.getAddr(), "addr");
+        Matcher matcher = PROXY_ADDR_PATTERN.matcher(normalized);
+        matcher.matches();
+        String host = matcher.group(1);
+        int grpcPort = proxy.getGrpcPort() > 0 ? proxy.getGrpcPort() : Integer.parseInt(matcher.group(2));
+        Integer remotingPort = proxy.getRemotingPort() > 0
+                ? Integer.valueOf(proxy.getRemotingPort()) : deriveRemotingPort(grpcPort);
+        ProbeTask task = new ProbeTask(normalized, grpcPort, remotingPort,
+                probeAsync(host, grpcPort), remotingPort != null ? probeAsync(host, remotingPort) : null);
+        awaitProbes(List.of(task));
+        return toTopologyVO(task);
+    }
+
+    public void reloadConfigForInstance(String instanceId, String addr) {
+        ProxyVO proxy = requireInstanceProxy(instanceId, addr);
+        reloadValidatedProxy(instanceId, normalizeProxyAddr(proxy.getAddr(), "addr"));
+    }
+
+    private ProxyVO requireInstanceProxy(String instanceId, String addr) {
+        String normalized = normalizeProxyAddr(addr, "addr");
+        List<ProxyVO> proxies = clusterService.listProxiesForInstance(instanceId);
+        return (proxies == null ? List.<ProxyVO>of() : proxies).stream()
+                .filter(proxy -> normalized.equals(proxy.getAddr()))
+                .findFirst()
+                .orElseThrow(() -> new BusinessException(404,
+                        "Proxy not found in instance " + instanceId + ": " + normalized));
+    }
+
+    private void reloadValidatedProxy(String normalizedClusterId, String normalized) {
         String url = "http://" + normalized + RELOAD_PATH;
         try {
             ResponseEntity<String> response = restTemplate.postForEntity(url, null, String.class);
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/common/config/LegacyJackson2Config.java
similarity index 53%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/common/config/LegacyJackson2Config.java
index 141b1cef2..87c1f2ad2 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/common/config/LegacyJackson2Config.java
@@ -14,26 +14,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.cluster.broker;
-
-
-import java.util.List;
-
-public interface ClusterProvider {
-
-    List<ClusterVO> discoverClusters();
-
-    default List<ClusterVO> discoverClusters(String instanceId) {
-        return discoverClusters();
+package org.apache.rocketmq.studio.common.config;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration(proxyBeanMethods = false)
+public class LegacyJackson2Config {
+
+    @Bean
+    public ObjectMapper jackson2ObjectMapper() {
+        return new ObjectMapper()
+                .findAndRegisterModules()
+                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+                .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
     }
 
-    default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
-        return List.of();
-    }
-
-    ClusterVO refreshClusterDetail(String clusterId);
-
-    default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
-        return refreshClusterDetail(clusterId);
-    }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
index 2fcbb55d6..dc27dbe31 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
@@ -17,11 +17,16 @@
 package org.apache.rocketmq.studio.common.exception;
 
 import lombok.Getter;
+import org.springframework.http.HttpStatus;
 
 @Getter
 public class BusinessException extends RuntimeException {
     private final int code;
 
+    public BusinessException(HttpStatus status, String message) {
+        this(status.value(), message);
+    }
+
     public BusinessException(int code, String message) {
         super(message);
         this.code = code;
diff --git a/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceResolver.java b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceResolver.java
new file mode 100644
index 000000000..d4f670d35
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/instance/InstanceResolver.java
@@ -0,0 +1,59 @@
+/*
+ * 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.rocketmq.studio.instance;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.provider.apache.RocketMQDefaultClusterResolver;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.util.Optional;
+
+@Component
+@RequiredArgsConstructor
+public class InstanceResolver {
+    private final InstanceRepository instanceRepository;
+    private final RocketMQDefaultClusterResolver defaultClusters;
+
+    public Optional<InstanceVO> findByName(String cluster) {
+        return instanceRepository.findByName(cluster).or(() -> defaultClusters.find(cluster));
+    }
+
+    public Optional<InstanceVO> findByIdentifier(String identifier) {
+        Optional<InstanceVO> registered = instanceRepository.findByIdentifier(identifier);
+        if (registered.isPresent() && (identifier.equals(registered.get().getName())
+                || instanceRepository.findByName(identifier).isPresent())) {
+            return registered;
+        }
+        return defaultClusters.find(identifier).or(() -> registered);
+    }
+
+    public String resolveClusterName(String identifier) {
+        if (!StringUtils.hasText(identifier) || instanceRepository.findByName(identifier).isPresent()) {
+            return null;
+        }
+        Optional<InstanceVO> configured = defaultClusters.find(identifier);
+        if (configured.isPresent()) {
+            return configured.get().getName();
+        }
+        if (instanceRepository.findByIdentifier(identifier).isPresent()) {
+            return null;
+        }
+        throw new BusinessException(404, "Instance not found: " + identifier);
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclRepository.java b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclRepository.java
index 8e247cbf0..1b8fa95b4 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclRepository.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclRepository.java
@@ -25,6 +25,8 @@ public interface AclRepository {
     PageResult<AclRuleVO> findRulePage(String principal, String resource, String scope,
             String decision, String aclVersion, int page, int pageSize);
 
+    Optional<AclRuleVO> findRuleById(Long id);
+
     AclRuleVO saveRule(AclRuleVO rule);
 
     /**
diff --git a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java
index 97b4b8dbe..b9a9c967c 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java
@@ -26,7 +26,7 @@ import org.apache.rocketmq.studio.common.util.CredentialUtils;
 import org.apache.rocketmq.studio.common.util.EntityIds;
 import org.apache.rocketmq.studio.audit.OperationAuditService;
 import org.apache.rocketmq.studio.model.Acl2PolicyContext;
-import org.apache.rocketmq.studio.instance.InstanceRepository;
+import org.apache.rocketmq.studio.instance.InstanceResolver;
 import org.apache.rocketmq.studio.instance.InstanceVO;
 import org.apache.rocketmq.studio.provider.tencent.TencentAclService;
 import lombok.RequiredArgsConstructor;
@@ -50,14 +50,14 @@ public class AclService {
 
     private final AclRepository aclRepository;
     private final OperationAuditService operationAuditService;
-    private final InstanceRepository instanceRepository;
+    private final InstanceResolver instanceResolver;
     private final TencentAclService tencentAclService;
 
     public AclCapabilitiesVO capabilities(String instanceId) {
         if (!StringUtils.hasText(instanceId)) {
             throw new BusinessException(400, "instanceId is required");
         }
-        InstanceVO instance = instanceRepository.findByIdentifier(instanceId)
+        InstanceVO instance = instanceResolver.findByIdentifier(instanceId)
                 .orElseThrow(() -> new BusinessException(404, "Instance not found: " + instanceId));
         if (instance.getVendor() == InstanceVendor.TENCENT) {
             return new AclCapabilitiesVO(instance.getId(), instance.getVendor(), instance.getType(),
@@ -106,6 +106,32 @@ public class AclService {
         return saved;
     }
 
+    public AclRuleVO getRule(String id, String instanceId) {
+        if (!StringUtils.hasText(id)) {
+            throw new BusinessException(400, "ACL rule id is required");
+        }
+        if (isTencentInstance(instanceId)) {
+            return tencentAclService.listRules(instanceId, null).stream()
+                    .filter(rule -> id.equals(rule.getPrincipal())
+                            || rule.getId() != null && id.equals(rule.getId().toString()))
+                    .findFirst()
+                    .orElseThrow(() -> new BusinessException(404, "ACL rule not found: " + id));
+        }
+        return aclRepository.findRuleById(EntityIds.parseId(id))
+                .orElseThrow(() -> new BusinessException(404, "ACL rule not found: " + id));
+    }
+
+    public AclUserVO getUser(String id, String instanceId) {
+        if (!StringUtils.hasText(id)) {
+            throw new BusinessException(400, "ACL user id is required");
+        }
+        return listUsers(instanceId).stream()
+                .filter(user -> id.equals(user.getUsername())
+                        || user.getId() != null && id.equals(user.getId().toString()))
+                .findFirst()
+                .orElseThrow(() -> new BusinessException(404, "ACL user not found: " + id));
+    }
+
     public AclRuleVO updateRule(AclRuleVO rule, String instanceId) {
         if (isTencentInstance(instanceId)) {
             return tencentAclService.updateRule(instanceId, rule);
@@ -332,7 +358,7 @@ public class AclService {
         if (!StringUtils.hasText(instanceId)) {
             return false;
         }
-        return instanceRepository.findByIdentifier(instanceId)
+        return instanceResolver.findByIdentifier(instanceId)
                 .map(instance -> instance.getVendor() == InstanceVendor.TENCENT)
                 .orElse(false);
     }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
index 0b569c922..a7d7894df 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
@@ -67,6 +67,12 @@ public class MybatisPlusAclRepository implements AclRepository {
                 (int) mapperPage.getSize());
     }
 
+    @Override
+    public Optional<AclRuleVO> findRuleById(Long id) {
+        return Optional.ofNullable(ruleMapper.selectById(id))
+                .map(MybatisPlusAclRepository::toRuleVO);
+    }
+
     @Override
     public AclRuleVO saveRule(AclRuleVO rule) {
         RmqAclRule entity = toRuleEntity(rule);
diff --git a/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQProvider.java b/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQProvider.java
index 3d36f7c60..186ddcd71 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/instance/dlq/DLQProvider.java
@@ -50,4 +50,4 @@ public interface DLQProvider {
      */
     DLQExcelExportResultVO exportExcel(String instanceId, String groupName, Long startTime, Long endTime,
                                        List<String> msgIds);
-}
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java
index b2a552c91..50e23f040 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/instance/topic/MetadataService.java
@@ -20,6 +20,7 @@ import org.apache.rocketmq.studio.audit.OperationAuditConstants.Operation;
 import org.apache.rocketmq.studio.audit.OperationAuditConstants.ResourceType;
 import org.apache.rocketmq.studio.audit.OperationAuditConstants.Result;
 import org.apache.rocketmq.studio.audit.OperationAuditService;
+import org.apache.rocketmq.studio.instance.InstanceResolver;
 import org.apache.rocketmq.studio.provider.apache.AdminClient;
 import org.apache.rocketmq.studio.provider.apache.ConsumerLagResolver;
 import org.apache.rocketmq.studio.provider.apache.MetadataProvider;
@@ -38,6 +39,8 @@ import org.apache.rocketmq.studio.instance.group.ConsumerGroupSettingsVO;
 import org.apache.rocketmq.studio.instance.group.QueueProgressVO;
 import org.apache.rocketmq.studio.instance.group.ResetConsumerOffsetPreviewVO;
 import org.apache.rocketmq.studio.instance.group.SubscriptionEntryVO;
+import org.apache.rocketmq.studio.instance.message.MessageRecordVO;
+import org.apache.rocketmq.studio.instance.message.MessageService;
 import org.apache.rocketmq.studio.provider.InstanceProvider;
 import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
 
@@ -48,7 +51,11 @@ import org.springframework.stereotype.Service;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
 import java.util.function.Supplier;
 
 @Slf4j
@@ -61,20 +68,20 @@ public class MetadataService {
     private final MetadataProvider metadataProvider;
     private final AdminClient adminClient;
     private final InstanceProviderRegistry providerRegistry;
-    private final org.apache.rocketmq.studio.instance.InstanceRepository instanceRepository;
+    private final InstanceResolver instanceResolver;
     private final OperationAuditService operationAuditService;
+    private final MessageService messageService;
 
     /**
-     * External callers address instances by their globally unique instance ID (name);
-     * internal storage keys rows by the numeric primary key. Resolve the identifier
-     * before any DB-backed lookup; unknown identifiers pass through unchanged so the
-     * downstream 404/empty semantics stay intact.
+     * Canonicalizes registered instance names and legacy numeric IDs, while preserving physical
+     * names from the configured connection. Metadata providers apply the corresponding storage
+     * scope separately; unresolved identifiers retain the downstream 404/empty semantics.
      */
     String normalizeInstanceId(String instanceId) {
         if (!StringUtils.hasText(instanceId)) {
             return instanceId;
         }
-        return instanceRepository.findByIdentifier(instanceId)
+        return instanceResolver.findByIdentifier(instanceId)
                 .map(org.apache.rocketmq.studio.instance.InstanceVO::getName)
                 .orElse(instanceId);
     }
@@ -93,7 +100,21 @@ public class MetadataService {
             return metadataProvider.listTopics(
                     normalizeFilter(clusterId), normalizeFilter(type), normalizeFilter(search));
         }
-        return resolve(instanceId).listTopics(instanceId, normalizeFilter(type), normalizeFilter(search));
+        return resolve(instanceId).listTopics(instanceId, normalizeFilter(type), normalizeFilter(search)).stream()
+                .filter(topic -> !StringUtils.hasText(clusterId) || clusterId.equals(topic.getClusterId())).toList();
+    }
+
+    public Optional<TopicVO> findTopic(
+            String instanceId, String clusterId, String name) {
+        String topicName = requireName(name, "topic name");
+        return unique(listTopics(instanceId, clusterId, null, topicName).stream()
+                .filter(topic -> topicName.equals(topic.getName())).toList(), "Topic " + topicName);
+    }
+
+    public TopicVO getTopic(String instanceId, String clusterId, String name) {
+        String topicName = requireName(name, "topic name");
+        return findTopic(instanceId, clusterId, topicName)
+                .orElseThrow(() -> new BusinessException(404, "Topic not found: " + topicName));
     }
 
     public PageResult<TopicVO> listTopicsPage(String instanceId, String clusterId, String type,
@@ -115,40 +136,44 @@ public class MetadataService {
 
 
     public TopicVO createTopic(TopicVO topic) {
+        return createTopic(topic == null ? null : topic.getInstanceId(), topic);
+    }
+
+    public TopicVO createTopic(String instanceId, TopicVO topic) {
         requireTopic(topic);
         if (SystemTopicFilter.isSystem(topic.getName())) {
             throw new BusinessException(400, "System topics cannot be created: " + topic.getName());
         }
-        String instanceId = topic.getInstanceId();
+        topic.setInstanceId(instanceId);
         InstanceProvider provider = resolve(instanceId);
         return executeWithAudit(provider, Operation.CREATE_TOPIC, ResourceType.TOPIC, topic.getName(),
                 instanceId, topicDetail(topic), () -> provider.createTopic(instanceId, topic));
     }
 
-
     public TopicVO updateTopic(TopicVO topic) {
+        return updateTopic(topic == null ? null : topic.getInstanceId(), topic);
+    }
+
+    public TopicVO updateTopic(String instanceId, TopicVO topic) {
         requireTopic(topic);
-        String instanceId = topic.getInstanceId();
+        topic.setInstanceId(instanceId);
         InstanceProvider provider = resolve(instanceId);
         return executeWithAudit(provider, Operation.UPDATE_TOPIC, ResourceType.TOPIC, topic.getName(),
                 instanceId, topicDetail(topic), () -> provider.updateTopic(instanceId, topic));
     }
 
-
     public void deleteTopic(String name) {
         deleteTopic(null, name);
     }
 
     public void deleteTopic(String instanceId, String name) {
-        instanceId = normalizeInstanceId(instanceId);
+        String target = normalizeInstanceId(instanceId);
         String topicName = requireName(name, "topic name");
-        InstanceProvider provider = resolve(instanceId);
-        String normalizedInstanceId = instanceId;
+        InstanceProvider provider = resolve(target);
         executeWithAudit(provider, Operation.DELETE_TOPIC, ResourceType.TOPIC,
-                topicName, instanceId, null, () -> provider.deleteTopic(normalizedInstanceId, topicName));
+                topicName, target, null, () -> provider.deleteTopic(target, topicName));
     }
 
-
     public List<BrokerRouteVO> getTopicRoutes(String name) {
         return getTopicRoutes(null, name);
     }
@@ -195,6 +220,43 @@ public class MetadataService {
         return adminClient.sendMessage(request);
     }
 
+    /**
+     * Re-publishes one stored message to a target topic. The original broker message is read
+     * through the instance-aware message service and only the application payload/properties are
+     * copied; broker offsets and delivery metadata are never reused.
+     */
+    public SendMessageVO resendMessage(String instanceId, String sourceTopic, String msgId, String targetTopic) {
+        MessageRecordVO original = findMessageForResend(instanceId, sourceTopic, msgId);
+        return resendMessage(instanceId, original, targetTopic);
+    }
+
+    public SendMessageVO resendMessage(String instanceId, MessageRecordVO original, String targetTopic) {
+        String destination = StringUtils.hasText(targetTopic) ? targetTopic.trim() : original.getTopic();
+        if (!StringUtils.hasText(destination)) {
+            throw new BusinessException(400, "target topic is required when the source message has no topic");
+        }
+        SendMessageDTO request = SendMessageDTO.builder()
+                .instanceId(normalizeInstanceId(instanceId))
+                .topic(destination)
+                .tag(original.getTag())
+                .key(original.getKey())
+                .body(original.getBody())
+                .properties(original.getProperties() == null ? null : Map.copyOf(original.getProperties()))
+                .build();
+        return sendMessage(request);
+    }
+
+    /** Loads the exact source message used by resend without exposing or publishing its body. */
+    public MessageRecordVO findMessageForResend(String instanceId, String sourceTopic, String msgId) {
+        String messageId = requireName(msgId, "message id");
+        List<MessageRecordVO> matches = messageService.queryMessages(
+                instanceId, normalizeFilter(sourceTopic), messageId, null, null, null, null);
+        return matches.stream()
+                .filter(message -> messageId.equals(message.getMsgId()))
+                .findFirst()
+                .orElseThrow(() -> new BusinessException(404, "Message not found: " + messageId));
+    }
+
     // ── ConsumerGroupVO ───────────────────────────────────────────────
 
 
@@ -207,7 +269,8 @@ public class MetadataService {
         if (!StringUtils.hasText(instanceId) && StringUtils.hasText(clusterId)) {
             return metadataProvider.listConsumerGroups(normalizeFilter(clusterId), normalizeFilter(search));
         }
-        return resolve(instanceId).listConsumerGroups(instanceId, normalizeFilter(search));
+        return resolve(instanceId).listConsumerGroups(instanceId, normalizeFilter(search)).stream()
+                .filter(group -> !StringUtils.hasText(clusterId) || clusterId.equals(group.getClusterId())).toList();
     }
 
     public PageResult<ConsumerGroupVO> listConsumerGroupsPage(String instanceId, String clusterId, String search,
@@ -247,13 +310,41 @@ public class MetadataService {
      * frontend keeps the existing row unchanged.
      */
     public ConsumerGroupVO refreshConsumerGroup(String instanceId, String name) {
-        instanceId = normalizeInstanceId(instanceId);
+        return findConsumerGroup(instanceId, name).orElse(null);
+    }
+
+    public Optional<ConsumerGroupVO> findConsumerGroup(String instanceId, String name) {
+        String normalizedInstanceId = normalizeInstanceId(instanceId);
         String groupName = requireName(name, "consumer group name");
-        return listConsumerGroups(instanceId, null, normalizeFilter(groupName))
-                .stream()
-                .filter(group -> groupName.equals(group.getName()))
-                .findFirst()
-                .orElse(null);
+        return unique(listConsumerGroups(normalizedInstanceId, null, groupName).stream()
+                .filter(group -> groupName.equals(group.getName())).toList(), "Consumer group " + groupName);
+    }
+
+    public List<ConsumerGroupVO> consumerGroupConfigurations(String instanceId, String name) {
+        List<ConsumerGroupVO> groups = listConsumerGroups(instanceId, null, name).stream()
+                .filter(group -> name.equals(group.getName())).toList();
+        if (groups.isEmpty()) throw new BusinessException(404, "Consumer group not found: " + name);
+        return groups;
+    }
+
+    /** Runtime statistics cover the Instance-wide group; repeated physical configurations are not summed. */
+    public ConsumerGroupVO consumerGroupRuntimeView(String instanceId, String name) {
+        List<ConsumerGroupVO> groups = consumerGroupConfigurations(instanceId, name);
+        if (groups.size() == 1) return groups.get(0);
+        ConsumerGroupVO runtime = getConsumerGroup(instanceId, name);
+        runtime.setInstanceId(normalizeInstanceId(instanceId));
+        return runtime;
+    }
+
+    public ConsumerGroupVO requireConsumerGroup(String instanceId, String name) {
+        return findConsumerGroup(instanceId, name)
+                .orElseThrow(() -> new BusinessException(404, "Consumer group not found: " + name));
+    }
+
+    private <T> Optional<T> unique(List<T> matches, String resource) {
+        if (matches.size() > 1) throw new BusinessException(409,
+                resource + " exists in multiple Broker clusters; a physical target is required for configuration changes");
+        return matches.stream().findFirst();
     }
 
 
@@ -280,11 +371,24 @@ public class MetadataService {
 
 
     public ConsumerGroupVO createConsumerGroup(ConsumerGroupVO group) {
-        String instanceId = group == null ? null : group.getInstanceId();
+        return saveConsumerGroup(group == null ? null : group.getInstanceId(), group, Operation.CREATE_GROUP,
+                (provider, instanceId) -> provider.createConsumerGroup(instanceId, group));
+    }
+
+    public ConsumerGroupVO updateConsumerGroup(ConsumerGroupVO group) {
+        return saveConsumerGroup(group == null ? null : group.getInstanceId(), group, Operation.UPDATE_GROUP,
+                (provider, instanceId) -> provider.updateConsumerGroup(instanceId, group));
+    }
+
+    private ConsumerGroupVO saveConsumerGroup(String instanceId, ConsumerGroupVO group, String operation,
+                                              BiFunction<InstanceProvider, String, ConsumerGroupVO> mutation) {
+        if (group != null) {
+            group.setInstanceId(instanceId);
+        }
         InstanceProvider provider = resolve(instanceId);
-        return executeWithAudit(provider, Operation.CREATE_GROUP, ResourceType.GROUP,
+        return executeWithAudit(provider, operation, ResourceType.GROUP,
                 group == null ? null : group.getName(), instanceId, consumerGroupDetail(group),
-                () -> provider.createConsumerGroup(instanceId, group));
+                () -> mutation.apply(provider, instanceId));
     }
 
     public ConsumerGroupSettingsVO getConsumerGroupSettings(String instanceId, String name) {
@@ -307,15 +411,17 @@ public class MetadataService {
     }
 
     public void deleteConsumerGroup(String instanceId, String name) {
-        instanceId = normalizeInstanceId(instanceId);
+        String target = normalizeInstanceId(instanceId);
+        deleteConsumerGroup(target, name, (provider, groupName) -> provider.deleteConsumerGroup(target, groupName));
+    }
+
+    private void deleteConsumerGroup(String instanceId, String name, BiConsumer<InstanceProvider, String> mutation) {
         String groupName = requireName(name, "consumer group name");
         InstanceProvider provider = resolve(instanceId);
-        String normalizedInstanceId = instanceId;
         executeWithAudit(provider, Operation.DELETE_GROUP, ResourceType.GROUP,
-                groupName, instanceId, null, () -> provider.deleteConsumerGroup(normalizedInstanceId, groupName));
+                groupName, instanceId, null, () -> mutation.accept(provider, groupName));
     }
 
-
     public void resetOffset(String name, long timestamp, String topic) {
         resetOffset(null, name, timestamp, topic);
     }
@@ -339,6 +445,35 @@ public class MetadataService {
                 () -> provider.resetOffset(normalizedInstanceId, groupName, timestamp, topicName));
     }
 
+    /**
+     * Advances a consumer group's offsets to the current time. When topic is omitted every
+     * currently registered subscription is advanced independently.
+     */
+    public List<String> skipAccumulated(String instanceId, String name, String topic) {
+        String groupName = requireName(name, "consumer group name");
+        List<String> topics = resolveSkipAccumulatedTopics(instanceId, groupName, topic);
+        long timestamp = System.currentTimeMillis();
+        topics.forEach(subscriptionTopic -> resetOffset(instanceId, groupName, timestamp, subscriptionTopic));
+        return topics;
+    }
+
+    /** Resolves the exact topic scope used by skip-accumulated without changing offsets. */
+    public List<String> resolveSkipAccumulatedTopics(String instanceId, String name, String topic) {
+        String groupName = requireName(name, "consumer group name");
+        List<String> topics = StringUtils.hasText(topic)
+                ? List.of(topic.trim())
+                : getGroupSubscriptions(instanceId, groupName).stream()
+                        .map(SubscriptionEntryVO::getTopic)
+                        .filter(StringUtils::hasText)
+                        .map(String::trim)
+                        .distinct()
+                        .toList();
+        if (topics.isEmpty()) {
+            throw new BusinessException(404, "No subscribed topics found for consumer group: " + groupName);
+        }
+        return topics;
+    }
+
 
     /**
      * Every metadata operation goes through the provider registry; a blank instance id keeps the
@@ -600,25 +735,18 @@ public class MetadataService {
 
     private void executeWithAudit(InstanceProvider provider, String operation, String resourceType,
                                   String resourceName, String instanceId, String detail, Runnable action) {
-        if (provider.vendor() == InstanceVendor.APACHE) {
-            action.run();
-            return;
-        }
-        try {
+        executeWithAudit(provider, operation, resourceType, resourceName, instanceId, detail, () -> {
             action.run();
-            recordAudit(operation, resourceType, resourceName, instanceId, detail, Result.SUCCESS, null);
-        } catch (RuntimeException failure) {
-            recordAudit(operation, resourceType, resourceName, instanceId, detail, Result.FAILED,
-                    failure.getMessage());
-            throw failure;
-        }
+            return null;
+        });
     }
 
     private void recordAudit(String operation, String resourceType, String resourceName, String instanceId,
                              String detail, String result, String errorMessage) {
         try {
-            operationAuditService.record(operation, resourceType, resourceName, instanceId,
-                    detail, result, errorMessage);
+            var instance = instanceResolver.findByIdentifier(instanceId).orElse(null);
+            operationAuditService.record(operation, resourceType, resourceName,
+                    instance == null ? instanceId : instance.getName(), detail, result, errorMessage);
         } catch (Exception auditFailure) {
             log.warn("Failed to record audit operation={} resource={}: {}", operation, resourceName,
                     auditFailure.getMessage());
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiController.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiController.java
index 4c675aac9..a2b9627bc 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiController.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiController.java
@@ -16,23 +16,15 @@
  */
 package org.apache.rocketmq.studio.ops.ai;
 
-import org.apache.rocketmq.studio.common.domain.Result;
 import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.common.domain.Result;
 import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
 @RestController
 @RequestMapping("/api/ai")
 @RequiredArgsConstructor
@@ -49,27 +41,4 @@ public class AiController {
     public Result<AiExecuteResultVO> execute(@RequestBody AiCommandDTO command) {
         return Result.ok(aiService.execute(command));
     }
-
-    @GetMapping("/tools")
-    public ResponseEntity<Result<List<AiToolVO>>> listTools(
-            @RequestParam(required = false) String cluster) {
-        List<AiToolVO> tools = cluster == null
-                ? aiService.listTools()
-                : aiService.listTools(cluster);
-        return ResponseEntity.ok()
-                .header("X-RMQ-Catalog-Version", aiService.catalogVersion())
-                .header("X-RMQ-Catalog-Digest", aiService.catalogDigest())
-                .header("X-RMQ-Minimum-Client-Version", aiService.minimumClientVersion())
-                .body(Result.ok(tools));
-    }
-
-    @PostMapping("/tools/{name}/execute")
-    public Result<Object> executeTool(
-            @PathVariable String name,
-            @RequestBody(required = false) Map<String, Object> input) {
-        Map<String, Object> normalizedInput = input == null
-                ? Collections.emptyMap()
-                : input;
-        return Result.ok(aiService.executeTool(name, normalizedInput));
-    }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiService.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiService.java
index 62ede2ee0..c30d27a9f 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiService.java
@@ -23,16 +23,12 @@ import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.springframework.stereotype.Service;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
-import java.util.List;
-import java.util.Map;
-
 @Slf4j
 @Service
 @RequiredArgsConstructor
 public class AiService {
 
     private final LlmGateway llmGateway;
-    private final McpServerRegistry mcpServerRegistry;
     private final ObjectMapper objectMapper;
 
 
@@ -74,33 +70,4 @@ public class AiService {
                     .build();
         }
     }
-
-
-    public List<AiToolVO> listTools() {
-        log.debug("Listing available AI tools");
-        return mcpServerRegistry.listTools();
-    }
-
-    public List<AiToolVO> listTools(String clusterId) {
-        log.debug("Listing available AI tools for cluster: {}", clusterId);
-        return mcpServerRegistry.listTools(clusterId);
-    }
-
-    public Object executeTool(String name, Map<String, Object> input) {
-        AiPayloadGuard.validateToolInvocation(name, input, objectMapper);
-        log.info("Executing registered AI tool: {}", name);
-        return mcpServerRegistry.execute(name, input);
-    }
-
-    public String catalogVersion() {
-        return mcpServerRegistry.catalogVersion();
-    }
-
-    public String catalogDigest() {
-        return mcpServerRegistry.catalogDigest();
-    }
-
-    public String minimumClientVersion() {
-        return mcpServerRegistry.minimumClientVersion();
-    }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolVO.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolCallDTO.java
similarity index 75%
copy from server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolVO.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolCallDTO.java
index 770602ddd..486ce7709 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolVO.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolCallDTO.java
@@ -21,21 +21,13 @@ import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
-import java.util.List;
+import java.util.Map;
 
 @Data
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class AiToolVO {
+public class AiToolCallDTO {
     private String name;
-    private String description;
-    private Object parameters;
-    private String riskLevel;
-    private String permission;
-    private List<String> requiredCapabilities;
-    private Object outputSchema;
-    private String viewHint;
-    private boolean deprecated;
-    private String replacement;
+    private Map<String, Object> arguments;
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolVO.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolVO.java
index 770602ddd..75c9d43ba 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolVO.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiToolVO.java
@@ -29,13 +29,17 @@ import java.util.List;
 @AllArgsConstructor
 public class AiToolVO {
     private String name;
+    private String version;
+    private Object cli;
     private String description;
     private Object parameters;
     private String riskLevel;
+    private String operationLevel;
     private String permission;
     private List<String> requiredCapabilities;
     private Object outputSchema;
     private String viewHint;
     private boolean deprecated;
     private String replacement;
+    private boolean implemented;
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/McpToolController.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/McpToolController.java
new file mode 100644
index 000000000..9ff75f9d6
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/McpToolController.java
@@ -0,0 +1,64 @@
+/*
+ * 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.rocketmq.studio.ops.ai;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.studio.common.domain.Result;
+import org.apache.rocketmq.studio.ops.ai.auth.McpAuthentication;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.apache.rocketmq.studio.ops.ai.tool.service.ToolExecutionService;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/mcp/tools")
+@RequiredArgsConstructor
+@ConditionalOnProperty(name = "spring.ai.mcp.server.enabled", havingValue = "true")
+public class McpToolController {
+
+    private final ToolExecutionService toolExecutor;
+    private final ObjectMapper objectMapper;
+
+    @PostMapping("/call")
+    public Result<Object> callTool(
+            @RequestBody(required = false) AiToolCallDTO call,
+            HttpServletRequest request) {
+        McpAuthentication authentication = authentication(request);
+        if (call == null) {
+            throw ToolError.TOOL_CALL_REQUIRED.exception();
+        }
+        AiPayloadGuard.validateToolInvocation(call.getName(), call.getArguments(), objectMapper);
+        log.info("Calling registered AI tool: {}", call.getName());
+        return Result.ok(toolExecutor.execute(
+                call.getName(), call.getArguments(), authentication));
+    }
+
+    private static McpAuthentication authentication(HttpServletRequest request) {
+        Object authentication = request.getAttribute(McpAuthentication.ATTRIBUTE);
+        if (authentication instanceof McpAuthentication mcpAuthentication) {
+            return mcpAuthentication;
+        }
+        throw ToolError.MCP_AUTHENTICATION_REQUIRED.exception();
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiController.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ToolController.java
similarity index 57%
copy from server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiController.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/ToolController.java
index 4c675aac9..9970b49ce 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/AiController.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ToolController.java
@@ -16,10 +16,12 @@
  */
 package org.apache.rocketmq.studio.ops.ai;
 
-import org.apache.rocketmq.studio.common.domain.Result;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.RequiredArgsConstructor;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.studio.common.domain.Result;
+import org.apache.rocketmq.studio.ops.ai.tool.service.ToolDiscoveryService;
+import org.apache.rocketmq.studio.ops.ai.tool.service.ToolExecutionService;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -27,49 +29,37 @@ import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
+@Slf4j
 @RestController
-@RequestMapping("/api/ai")
+@RequestMapping("/api/ai/tools")
 @RequiredArgsConstructor
-public class AiController {
+public class ToolController {
 
-    private final AiService aiService;
-
-    @PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
-    public SseEmitter chat(@RequestBody ChatDTO request) {
-        return aiService.chat(request);
-    }
+    private final ToolDiscoveryService toolDiscoveryService;
+    private final ToolExecutionService toolExecutor;
+    private final ObjectMapper objectMapper;
 
-    @PostMapping("/execute")
-    public Result<AiExecuteResultVO> execute(@RequestBody AiCommandDTO command) {
-        return Result.ok(aiService.execute(command));
+    @GetMapping
+    public Result<List<AiToolVO>> listTools(
+            @RequestParam String cluster) {
+        return Result.ok(toolDiscoveryService.listTools(cluster));
     }
 
-    @GetMapping("/tools")
-    public ResponseEntity<Result<List<AiToolVO>>> listTools(
-            @RequestParam(required = false) String cluster) {
-        List<AiToolVO> tools = cluster == null
-                ? aiService.listTools()
-                : aiService.listTools(cluster);
-        return ResponseEntity.ok()
-                .header("X-RMQ-Catalog-Version", aiService.catalogVersion())
-                .header("X-RMQ-Catalog-Digest", aiService.catalogDigest())
-                .header("X-RMQ-Minimum-Client-Version", aiService.minimumClientVersion())
-                .body(Result.ok(tools));
-    }
-
-    @PostMapping("/tools/{name}/execute")
+    @PostMapping("/{name}/execute")
     public Result<Object> executeTool(
             @PathVariable String name,
             @RequestBody(required = false) Map<String, Object> input) {
         Map<String, Object> normalizedInput = input == null
                 ? Collections.emptyMap()
                 : input;
-        return Result.ok(aiService.executeTool(name, normalizedInput));
+        AiPayloadGuard.validateToolInvocation(name, normalizedInput, objectMapper);
+        log.info("Executing registered AI tool: {}", name);
+        return Result.ok(toolExecutor.execute(name, normalizedInput));
     }
+
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ToolExceptionHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ToolExceptionHandler.java
new file mode 100644
index 000000000..45cca73db
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ToolExceptionHandler.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.rocketmq.studio.ops.ai;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionException;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+
+import java.util.Map;
+
+@Slf4j
+@Order(Ordered.HIGHEST_PRECEDENCE)
+@RestControllerAdvice(assignableTypes = {ToolController.class, McpToolController.class})
+public class ToolExceptionHandler {
+
+    @ExceptionHandler(ToolExecutionException.class)
+    public ResponseEntity<Map<String, String>> handleToolExecutionException(
+            ToolExecutionException exception) {
+        if (exception.getCode() >= HttpStatus.INTERNAL_SERVER_ERROR.value()) {
+            log.error("Tool execution failed: code={}, message={}",
+                    exception.getErrorCode(), exception.getMessage(), exception);
+        } else {
+            log.warn("Tool execution failed: code={}, message={}",
+                    exception.getErrorCode(), exception.getMessage());
+        }
+        return error(exception);
+    }
+
+    @ExceptionHandler(HttpMessageNotReadableException.class)
+    public ResponseEntity<Map<String, String>> handleUnreadableBody() {
+        return error(ToolError.REQUEST_BODY_INVALID.exception());
+    }
+
+    @ExceptionHandler(MissingServletRequestParameterException.class)
+    public ResponseEntity<Map<String, String>> handleMissingParameter(
+            MissingServletRequestParameterException exception) {
+        return error(ToolError.REQUEST_PARAMETER_REQUIRED.exception(exception.getParameterName()));
+    }
+
+    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+    public ResponseEntity<Map<String, String>> handleInvalidParameter(
+            MethodArgumentTypeMismatchException exception) {
+        return error(ToolError.REQUEST_PARAMETER_INVALID.exception(exception.getName()));
+    }
+
+    private static ResponseEntity<Map<String, String>> error(ToolExecutionException exception) {
+        return ResponseEntity.status(exception.getCode()).body(Map.of(
+                "code", exception.getErrorCode(),
+                "message", exception.getMessage(),
+                "hint", exception.getHint()));
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthentication.java
similarity index 60%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthentication.java
index 141b1cef2..b5911beb6 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthentication.java
@@ -14,26 +14,22 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.cluster.broker;
+package org.apache.rocketmq.studio.ops.ai.auth;
 
+public record McpAuthentication(
+        String cluster,
+        String principal) {
 
-import java.util.List;
+    public static final String ATTRIBUTE = McpAuthentication.class.getName();
 
-public interface ClusterProvider {
-
-    List<ClusterVO> discoverClusters();
-
-    default List<ClusterVO> discoverClusters(String instanceId) {
-        return discoverClusters();
+    public McpAuthentication {
+        requireText(cluster, "cluster");
+        requireText(principal, "principal");
     }
 
-    default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
-        return List.of();
-    }
-
-    ClusterVO refreshClusterDetail(String clusterId);
-
-    default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
-        return refreshClusterDetail(clusterId);
+    private static void requireText(String value, String field) {
+        if (value == null || value.isBlank()) {
+            throw new IllegalArgumentException(field + " must not be blank");
+        }
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticationException.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticationException.java
index 2fcbb55d6..bea10eb8a 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticationException.java
@@ -14,16 +14,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.auth;
 
-import lombok.Getter;
+public class McpAuthenticationException extends RuntimeException {
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
+    public McpAuthenticationException(String message) {
         super(message);
-        this.code = code;
+    }
+
+    public McpAuthenticationException(String message, Throwable cause) {
+        super(message, cause);
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticationFilter.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticationFilter.java
new file mode 100644
index 000000000..c019d2d96
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticationFilter.java
@@ -0,0 +1,84 @@
+/*
+ * 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.rocketmq.studio.ops.ai.auth;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.studio.auth.AuthenticatedUserContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.Map;
+
+@RequiredArgsConstructor
+@Slf4j
+public class McpAuthenticationFilter extends OncePerRequestFilter {
+
+    private final McpAuthenticator authenticator;
+    private final ObjectMapper objectMapper;
+
+    @Override
+    protected void doFilterInternal(
+            HttpServletRequest request,
+            HttpServletResponse response,
+            FilterChain filterChain) throws ServletException, IOException {
+        final McpAuthentication authentication;
+        try {
+            authentication = authenticator.authenticate(request);
+        } catch (McpAuthenticationException exception) {
+            log.warn("MCP authentication failed: path={}, remoteAddress={}, status={}, message={}",
+                    request.getRequestURI(), request.getRemoteAddr(),
+                    HttpStatus.UNAUTHORIZED.value(), exception.getMessage());
+            writeError(response, ToolError.MCP_AUTHENTICATION_FAILED, exception.getMessage());
+            return;
+        } catch (RuntimeException exception) {
+            log.error("MCP authentication failed unexpectedly: path={}, remoteAddress={}",
+                    request.getRequestURI(), request.getRemoteAddr(), exception);
+            writeError(response, ToolError.MCP_AUTHENTICATION_INTERNAL_ERROR);
+            return;
+        }
+        request.setAttribute(McpAuthentication.ATTRIBUTE, authentication);
+        try {
+            AuthenticatedUserContext.setUser(null, authentication.principal(), false);
+            filterChain.doFilter(request, response);
+        } finally {
+            AuthenticatedUserContext.clear();
+        }
+    }
+
+    private void writeError(HttpServletResponse response, ToolError error, Object... arguments)
+            throws IOException {
+        response.setStatus(error.httpStatus().value());
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        if (error.httpStatus() == HttpStatus.UNAUTHORIZED) {
+            response.setHeader(HttpHeaders.WWW_AUTHENTICATE, McpAuthenticator.ALGORITHM);
+        }
+        objectMapper.writeValue(response.getOutputStream(), Map.of(
+                "code", error.code(),
+                "message", error.message(arguments),
+                "hint", error.hint()));
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticator.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticator.java
new file mode 100644
index 000000000..5d65ccb62
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthenticator.java
@@ -0,0 +1,222 @@
+/*
+ * 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.rocketmq.studio.ops.ai.auth;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.apache.rocketmq.studio.cluster.broker.MqAdminProperties;
+import org.apache.rocketmq.studio.cluster.broker.RuntimeAdminClientResolver;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.instance.InstanceResolver;
+import org.apache.rocketmq.studio.instance.InstanceVO;
+import org.apache.rocketmq.studio.provider.credential.CloudCredentialRepository;
+import org.apache.rocketmq.studio.provider.credential.CloudCredentialVO;
+import org.springframework.http.HttpHeaders;
+import org.springframework.util.StringUtils;
+import org.springframework.web.util.UriUtils;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.time.Clock;
+import java.time.Duration;
+import java.util.HexFormat;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class McpAuthenticator {
+
+    static final String ALGORITHM = "RMQ-HMAC-SHA256";
+    static final String HEADER_CLUSTER = "X-RMQ-Cluster";
+    static final String HEADER_TIMESTAMP = "X-RMQ-Timestamp";
+    static final String AUTHENTICATION_FAILED_MESSAGE = "MCP authentication failed.";
+    static final Duration MAX_CLOCK_SKEW = Duration.ofMinutes(5);
+    private static final Pattern AUTHORIZATION = Pattern.compile(
+            "^" + Pattern.quote(ALGORITHM)
+                    + " Credential=([^,]+), Signature=([0-9a-f]{64})$");
+    private final InstanceResolver instanceResolver;
+    private final RuntimeAdminClientResolver adminClientResolver;
+    private final CloudCredentialRepository cloudCredentialRepository;
+    private final Clock clock;
+
+    public McpAuthenticator(
+            InstanceResolver instanceResolver,
+            RuntimeAdminClientResolver adminClientResolver,
+            CloudCredentialRepository cloudCredentialRepository) {
+        this(instanceResolver, adminClientResolver, cloudCredentialRepository, Clock.systemUTC());
+    }
+
+    McpAuthenticator(
+            InstanceResolver instanceResolver,
+            RuntimeAdminClientResolver adminClientResolver,
+            CloudCredentialRepository cloudCredentialRepository,
+            Clock clock) {
+        this.instanceResolver = instanceResolver;
+        this.adminClientResolver = adminClientResolver;
+        this.cloudCredentialRepository = cloudCredentialRepository;
+        this.clock = clock;
+    }
+
+    public McpAuthentication authenticate(HttpServletRequest request) {
+        Authorization authorization = authorization(request.getHeader(HttpHeaders.AUTHORIZATION));
+        String cluster = requiredHeader(request, HEADER_CLUSTER);
+        String timestamp = requiredHeader(request, HEADER_TIMESTAMP);
+        verifyTimestamp(timestamp);
+        InstanceVO instance = resolveInstance(cluster);
+        Credential credential = resolveCredential(instance);
+        verifySignature(request, authorization, credential, cluster, timestamp);
+        return new McpAuthentication(cluster, authorization.accessKey());
+    }
+
+    private Authorization authorization(String value) {
+        if (value == null || value.isBlank()) {
+            throw new McpAuthenticationException("MCP authentication is required.");
+        }
+        Matcher matcher = AUTHORIZATION.matcher(value);
+        if (!matcher.matches()) {
+            throw authenticationFailed();
+        }
+        final String accessKey;
+        try {
+            accessKey = UriUtils.decode(matcher.group(1), StandardCharsets.UTF_8);
+        } catch (IllegalArgumentException exception) {
+            throw authenticationFailed();
+        }
+        if (accessKey.isBlank() || !accessKey.equals(accessKey.trim())) {
+            throw authenticationFailed();
+        }
+        return new Authorization(accessKey, HexFormat.of().parseHex(matcher.group(2)));
+    }
+
+    private String requiredHeader(HttpServletRequest request, String name) {
+        String value = request.getHeader(name);
+        if (value == null) {
+            throw authenticationFailed();
+        }
+        if (value.isBlank() || !value.equals(value.trim())) {
+            throw authenticationFailed();
+        }
+        return value;
+    }
+
+    private InstanceVO resolveInstance(String cluster) {
+        try {
+            return instanceResolver.findByName(cluster).orElseThrow(McpAuthenticator::authenticationFailed);
+        } catch (BusinessException exception) {
+            if (exception.getCode() == 422) {
+                throw authenticationFailed(exception);
+            }
+            throw exception;
+        }
+    }
+
+    private Credential resolveCredential(InstanceVO instance) {
+        if (instance.getVendor() == null || instance.getVendor() == InstanceVendor.APACHE) {
+            try {
+                MqAdminProperties.Credential credential =
+                        adminClientResolver.resolveCredential(instance);
+                return new Credential(credential.getAccessKey().trim(), credential.getSecretKey().trim());
+            } catch (BusinessException exception) {
+                throw authenticationFailed(exception);
+            }
+        }
+        if (instance.getCredentialId() == null) {
+            throw authenticationFailed();
+        }
+        CloudCredentialVO credential = cloudCredentialRepository.findById(instance.getCredentialId())
+                .orElseThrow(McpAuthenticator::authenticationFailed);
+
+        if (!StringUtils.hasText(credential.getAccessKey())
+                || !StringUtils.hasText(credential.getSecretKey())) {
+            throw authenticationFailed();
+        }
+        return new Credential(credential.getAccessKey().trim(), credential.getSecretKey().trim());
+    }
+
+    private static void verifySignature(HttpServletRequest request, Authorization authorization, Credential credential,
+                                 String cluster, String timestamp) {
+        if (!authorization.accessKey().equals(credential.accessKey())) {
+            throw authenticationFailed();
+        }
+        byte[] expected = hmac(credential.secretKey(),
+                canonicalRequest(authorization.accessKey(), cluster, timestamp,
+                        request.getMethod(), requestTarget(request)));
+        if (!MessageDigest.isEqual(expected, authorization.signature())) {
+            throw authenticationFailed();
+        }
+    }
+
+    private void verifyTimestamp(String timestamp) {
+        final long requestTime;
+        try {
+            requestTime = Long.parseLong(timestamp);
+        } catch (NumberFormatException exception) {
+            throw authenticationFailed(exception);
+        }
+        long now = clock.millis();
+        long allowedSkew = MAX_CLOCK_SKEW.toMillis();
+        if (requestTime < now - allowedSkew || requestTime > now + allowedSkew) {
+            throw authenticationFailed();
+        }
+    }
+
+    private static byte[] hmac(String secretKey, String canonical) {
+        try {
+            Mac mac = Mac.getInstance("HmacSHA256");
+            mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
+            return mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8));
+        } catch (GeneralSecurityException exception) {
+            throw new IllegalStateException("HmacSHA256 is unavailable", exception);
+        }
+    }
+
+    private static String requestTarget(HttpServletRequest request) {
+        String query = request.getQueryString();
+        return request.getRequestURI() + (query == null ? "" : "?" + query);
+    }
+
+    static String canonicalRequest(
+            String accessKey,
+            String cluster,
+            String timestamp,
+            String method,
+            String requestTarget) {
+        return String.join("\n",
+                ALGORITHM,
+                accessKey,
+                cluster,
+                timestamp,
+                method,
+                requestTarget);
+    }
+
+    private static McpAuthenticationException authenticationFailed() {
+        return new McpAuthenticationException(AUTHENTICATION_FAILED_MESSAGE);
+    }
+
+    private static McpAuthenticationException authenticationFailed(Throwable cause) {
+        return new McpAuthenticationException(AUTHENTICATION_FAILED_MESSAGE, cause);
+    }
+
+    private record Authorization(String accessKey, byte[] signature) {
+    }
+
+    private record Credential(String accessKey, String secretKey) {
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/mcp/McpConfiguration.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/mcp/McpConfiguration.java
new file mode 100644
index 000000000..461a5743e
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/mcp/McpConfiguration.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.rocketmq.studio.ops.ai.mcp;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper;
+import io.modelcontextprotocol.server.McpServerFeatures;
+import org.apache.rocketmq.studio.ops.ai.auth.McpAuthenticationFilter;
+import org.apache.rocketmq.studio.ops.ai.auth.McpAuthenticator;
+import org.apache.rocketmq.studio.ops.ai.auth.McpAuthentication;
+import org.apache.rocketmq.studio.cluster.broker.RuntimeAdminClientResolver;
+import org.apache.rocketmq.studio.instance.InstanceResolver;
+import org.apache.rocketmq.studio.provider.credential.CloudCredentialRepository;
+import org.apache.rocketmq.studio.ops.ai.tool.catalog.ToolCatalog;
+import org.apache.rocketmq.studio.ops.ai.tool.service.ToolExecutionService;
+import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties;
+import org.springframework.ai.mcp.server.webmvc.transport.WebMvcStreamableServerTransportProvider;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.function.ServerRequest;
+import tools.jackson.databind.json.JsonMapper;
+
+import java.util.List;
+import java.util.Map;
+
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnProperty(name = "spring.ai.mcp.server.enabled", havingValue = "true")
+public class McpConfiguration {
+
+    private static final String TOOL_CALL_ENDPOINT = "/api/mcp/tools/call";
+
+    @Bean
+    public McpAuthenticator mcpAuthenticator(
+            InstanceResolver instanceResolver,
+            RuntimeAdminClientResolver adminClientResolver,
+            CloudCredentialRepository cloudCredentialRepository) {
+        return new McpAuthenticator(instanceResolver, adminClientResolver, cloudCredentialRepository);
+    }
+
+    @Bean
+    public FilterRegistrationBean<McpAuthenticationFilter> mcpAuthenticationFilterRegistration(
+            McpAuthenticator authenticator,
+            ObjectMapper objectMapper,
+            McpServerStreamableHttpProperties serverProperties) {
+        FilterRegistrationBean<McpAuthenticationFilter> registration =
+                new FilterRegistrationBean<>(new McpAuthenticationFilter(authenticator, objectMapper));
+        registration.addUrlPatterns(
+                serverProperties.getMcpEndpoint(),
+                TOOL_CALL_ENDPOINT);
+        return registration;
+    }
+
+    @Bean
+    public WebMvcStreamableServerTransportProvider webMvcStreamableServerTransportProvider(
+            @Qualifier("mcpServerJsonMapper") JsonMapper jsonMapper,
+            McpServerStreamableHttpProperties serverProperties) {
+        return WebMvcStreamableServerTransportProvider.builder()
+                .jsonMapper(new JacksonMcpJsonMapper(jsonMapper))
+                .mcpEndpoint(serverProperties.getMcpEndpoint())
+                .keepAliveInterval(serverProperties.getKeepAliveInterval())
+                .disallowDelete(serverProperties.isDisallowDelete())
+                .contextExtractor(McpConfiguration::extractContext)
+                .build();
+    }
+
+    @Bean
+    public List<McpServerFeatures.SyncToolSpecification> toolSpecifications(
+            ToolCatalog toolCatalog,
+            ToolExecutionService toolExecutor,
+            ObjectMapper objectMapper) {
+        return McpToolRegistrar.toToolSpecifications(toolCatalog, toolExecutor, objectMapper);
+    }
+
+    static McpTransportContext extractContext(ServerRequest request) {
+        Object authentication = request.servletRequest()
+                .getAttribute(McpAuthentication.ATTRIBUTE);
+        if (!(authentication instanceof McpAuthentication mcpAuthentication)) {
+            return McpTransportContext.EMPTY;
+        }
+        return McpTransportContext.create(Map.of(
+                McpAuthentication.ATTRIBUTE, mcpAuthentication));
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/mcp/McpToolRegistrar.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/mcp/McpToolRegistrar.java
new file mode 100644
index 000000000..cd5074ab1
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/mcp/McpToolRegistrar.java
@@ -0,0 +1,141 @@
+/*
+ * 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.rocketmq.studio.ops.ai.mcp;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.server.McpServerFeatures;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.rocketmq.studio.ops.ai.auth.McpAuthentication;
+import org.apache.rocketmq.studio.ops.ai.tool.catalog.ToolCatalog;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolDefinition;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionException;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolRiskLevel;
+import org.apache.rocketmq.studio.ops.ai.tool.service.ToolExecutionService;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+public class McpToolRegistrar {
+
+    private McpToolRegistrar() {
+    }
+
+    public static List<McpServerFeatures.SyncToolSpecification> toToolSpecifications(
+            ToolCatalog toolCatalog,
+            ToolExecutionService toolExecutor,
+            ObjectMapper objectMapper) {
+        return toolCatalog.list().stream()
+                .map(definition -> toolSpecification(definition, toolExecutor, objectMapper))
+                .toList();
+    }
+
+    public static McpServerFeatures.SyncToolSpecification toolSpecification(
+            ToolDefinition definition,
+            ToolExecutionService toolExecutor,
+            ObjectMapper objectMapper) {
+        BiFunction<McpSyncServerExchange, McpSchema.CallToolRequest, McpSchema.CallToolResult> handler =
+                (exchange, request) -> callTool(
+                        definition, exchange, request, toolExecutor, objectMapper);
+        return McpServerFeatures.SyncToolSpecification.builder()
+                .tool(tool(definition))
+                .callHandler(handler)
+                .build();
+    }
+
+    public static McpSchema.CallToolResult callTool(
+            ToolDefinition definition,
+            McpSyncServerExchange exchange,
+            McpSchema.CallToolRequest request,
+            ToolExecutionService toolExecutionService,
+            ObjectMapper objectMapper) {
+        McpAuthentication authentication = authentication(exchange);
+        if (authentication == null) {
+            return errorResult(ToolError.MCP_AUTHENTICATION_CONTEXT_MISSING.exception(), objectMapper);
+        }
+        try {
+            Object result = toolExecutionService.execute(
+                    definition.name(), request.arguments(), authentication);
+            return McpSchema.CallToolResult.builder()
+                    .content(List.of(McpSchema.TextContent.builder(serialize(result, objectMapper)).build()))
+                    .structuredContent(result)
+                    .isError(false)
+                    .build();
+        } catch (ToolExecutionException exception) {
+            return errorResult(exception, objectMapper);
+        }
+    }
+
+    public static McpSchema.Tool tool(ToolDefinition definition) {
+        String description = definition.description();
+        if (!definition.requiredCapabilities().isEmpty()) {
+            description += "\nRequires all capabilities: "
+                    + String.join(", ", definition.requiredCapabilities()) + ".";
+        }
+        return McpSchema.Tool.builder(definition.name(), definition.inputSchema())
+                .description(description)
+                .outputSchema(definition.outputSchema())
+                .annotations(toolAnnotations(definition))
+                .build();
+    }
+
+    public static McpSchema.ToolAnnotations toolAnnotations(ToolDefinition definition) {
+        boolean readOnly = definition.isReadOnly();
+        return McpSchema.ToolAnnotations.builder()
+                .title(definition.name())
+                .readOnlyHint(readOnly)
+                .destructiveHint(definition.riskLevel() == ToolRiskLevel.L3)
+                .idempotentHint(false)
+                .openWorldHint(true)
+                .build();
+    }
+
+    private static McpAuthentication authentication(McpSyncServerExchange exchange) {
+        McpTransportContext context = exchange == null ? null : exchange.transportContext();
+        Object authentication = context == null
+                ? null
+                : context.get(McpAuthentication.ATTRIBUTE);
+        return authentication instanceof McpAuthentication mcpAuthentication ? mcpAuthentication : null;
+    }
+
+    private static McpSchema.CallToolResult errorResult(
+            ToolExecutionException exception,
+            ObjectMapper objectMapper) {
+        Map<String, Object> error = Map.of(
+                "code", exception.getErrorCode(),
+                "message", exception.getMessage(),
+                "hint", exception.getHint());
+        return McpSchema.CallToolResult.builder()
+                .content(List.of(McpSchema.TextContent.builder(serialize(error, objectMapper)).build()))
+                .structuredContent(error)
+                .isError(true)
+                .build();
+    }
+
+    private static String serialize(Object value, ObjectMapper objectMapper) {
+        try {
+            return objectMapper.writeValueAsString(value);
+        } catch (JsonProcessingException e) {
+            throw new IllegalStateException("Unable to serialize MCP tool result", e);
+        }
+    }
+
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/AlertRuleListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/AlertRuleListToolHandler.java
deleted file mode 100644
index b001b5c51..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/AlertRuleListToolHandler.java
+++ /dev/null
@@ -1,82 +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.rocketmq.studio.ops.ai.tool;
-
-import lombok.RequiredArgsConstructor;
-import org.apache.rocketmq.studio.ops.alert.AlertRuleVO;
-import org.apache.rocketmq.studio.ops.alert.AlertService;
-import org.apache.rocketmq.studio.common.domain.PageResult;
-import org.springframework.stereotype.Component;
-
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-@Component
-@RequiredArgsConstructor
-public class AlertRuleListToolHandler implements ToolHandler {
-
-    private static final String NAME = "rmq.alert.rule.list";
-
-    private final AlertService alertService;
-
-    @Override
-    public String name() {
-        return NAME;
-    }
-
-    @Override
-    public Object execute(Map<String, Object> input) {
-        String search = (String) input.get("search");
-        Boolean enabled = (Boolean) input.get("enabled");
-        PageResult<AlertRuleVO> page = alertService.listRules(
-                search, enabled, ToolListPagination.page(input), ToolListPagination.pageSize(input));
-        return ToolListPagination.pagedResult(page, page.getItems().stream()
-                .map(AlertRuleListToolHandler::safeProjection)
-                .toList());
-    }
-
-    private static Map<String, Object> safeProjection(AlertRuleVO rule) {
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("id", rule.getId());
-        result.put("name", require(rule.getName(), "name"));
-        result.put("metric", require(rule.getMetric(), "metric"));
-        result.put("operator", blankIfNull(rule.getOperator()));
-        result.put("threshold", rule.getThreshold());
-        result.put("thresholdUnit", blankIfNull(rule.getThresholdUnit()));
-        result.put("duration", blankIfNull(rule.getDuration()));
-        result.put("channels", copyList(rule.getChannels()));
-        result.put("enabled", rule.isEnabled());
-        result.put("description", blankIfNull(rule.getDescription()));
-        return result;
-    }
-
-    private static String require(String value, String field) {
-        if (value == null || value.isBlank()) {
-            throw new IllegalStateException("Alert rule " + field + " is unavailable");
-        }
-        return value;
-    }
-
-    private static List<String> copyList(List<String> value) {
-        return value == null ? List.of() : List.copyOf(value);
-    }
-
-    private static String blankIfNull(String value) {
-        return value == null ? "" : value;
-    }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/CapabilitiesToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/CapabilitiesToolHandler.java
deleted file mode 100644
index 9b06eb25b..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/CapabilitiesToolHandler.java
+++ /dev/null
@@ -1,64 +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.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.cluster.broker.ClusterService;
-import org.apache.rocketmq.studio.cluster.broker.ClusterVO;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-@Component
-@RequiredArgsConstructor
-public class CapabilitiesToolHandler implements ToolHandler {
-
-    private static final String NAME = "rmq.capabilities";
-
-    private final ClusterService clusterService;
-    private final CapabilityResolver capabilityResolver;
-
-    @Override
-    public String name() {
-        return NAME;
-    }
-
-    @Override
-    public Object execute(Map<String, Object> input) {
-        String clusterId = (String) input.get("cluster");
-        ClusterVO cluster = clusterService.getCluster(clusterId);
-        List<String> capabilities = capabilityResolver.resolve(cluster);
-
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("cluster", blankIfNull(cluster.getId()));
-        result.put("type", cluster.getType() == null ? "" : cluster.getType().name());
-        result.put("version", blankIfNull(cluster.getVersion()));
-        result.put("capabilities", capabilities);
-        return result;
-    }
-
-    /**
-     * The output schema declares cluster/type/version as required strings; providers that
-     * do not populate them (notably the Apache runtime provider, which reports no cluster
-     * version) must not emit nulls or schema validation turns the tool call into a 500.
-     */
-    private static String blankIfNull(String value) {
-        return value == null ? "" : value;
-    }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/CapabilityResolver.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/CapabilityResolver.java
deleted file mode 100644
index 551adc043..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/CapabilityResolver.java
+++ /dev/null
@@ -1,64 +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.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.cluster.broker.ClusterService;
-import org.apache.rocketmq.studio.cluster.broker.ClusterVO;
-import org.apache.rocketmq.studio.common.exception.BusinessException;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.List;
-
-@Component
-@RequiredArgsConstructor
-public class CapabilityResolver {
-
-    private final ClusterService clusterService;
-
-    public List<String> resolve(String clusterId) {
-        return resolve(clusterService.getCluster(clusterId));
-    }
-
-    List<String> resolve(ClusterVO cluster) {
-        if (cluster.getType() == null) {
-            throw new BusinessException(
-                    400, "Cluster type is unavailable: " + cluster.getId());
-        }
-        return switch (cluster.getType()) {
-            case V4_DIRECT -> List.of(
-                    "REMOTING",
-                    "ROCKETMQ_4");
-            case V5_PROXY_LOCAL -> List.of(
-                    "ACL_V2",
-                    "GRPC",
-                    "LITE_TOPIC",
-                    "LOCAL_PROXY",
-                    "POP",
-                    "REMOTING",
-                    "ROCKETMQ_5");
-            case V5_PROXY_CLUSTER -> List.of(
-                    "ACL_V2",
-                    "CLUSTER_PROXY",
-                    "GRPC",
-                    "LITE_TOPIC",
-                    "POP",
-                    "REMOTING",
-                    "ROCKETMQ_5");
-        };
-    }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ClusterListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ClusterListToolHandler.java
deleted file mode 100644
index 67e6836c1..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ClusterListToolHandler.java
+++ /dev/null
@@ -1,73 +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.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.cluster.broker.ClusterService;
-import org.apache.rocketmq.studio.cluster.broker.ClusterVO;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-@Component
-@RequiredArgsConstructor
-public class ClusterListToolHandler implements ToolHandler {
-
-    private static final String NAME = "rmq.cluster.list";
-
-    private final ClusterService clusterService;
-
-    @Override
-    public String name() {
-        return NAME;
-    }
-
-    @Override
-    public Object execute(Map<String, Object> input) {
-        return clusterService.listClusters().stream()
-                .map(ClusterListToolHandler::safeProjection)
-                .toList();
-    }
-
-    private static Map<String, Object> safeProjection(ClusterVO cluster) {
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("id", blankIfNull(cluster.getId()));
-        result.put("name", blankIfNull(cluster.getName()));
-        result.put("type", requiredEnumName(cluster.getType(), "type", cluster.getId()));
-        result.put("status", requiredEnumName(cluster.getStatus(), "status", cluster.getId()));
-        result.put("version", blankIfNull(cluster.getVersion()));
-        return result;
-    }
-
-    /**
-     * The output schema declares id/name/version as required strings; providers that do
-     * not populate them (notably the Apache runtime provider, which reports no cluster
-     * version) must not emit nulls or schema validation turns the tool call into a 500.
-     */
-    private static String blankIfNull(String value) {
-        return value == null ? "" : value;
-    }
-
-    private static String requiredEnumName(Enum<?> value, String field, String clusterId) {
-        if (value == null) {
-            throw new IllegalStateException(
-                    "Cluster " + field + " is unavailable: " + clusterId);
-        }
-        return value.name();
-    }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ConsumerGroupListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ConsumerGroupListToolHandler.java
deleted file mode 100644
index 87792fed5..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ConsumerGroupListToolHandler.java
+++ /dev/null
@@ -1,92 +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.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
-import org.apache.rocketmq.studio.instance.topic.MetadataService;
-import org.apache.rocketmq.studio.provider.apache.ConsumerLagResolver;
-import org.apache.rocketmq.studio.common.domain.PageResult;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-@Component
-@RequiredArgsConstructor
-public class ConsumerGroupListToolHandler implements ToolHandler {
-
-    private static final String NAME = "rmq.group.list";
-
-    private final MetadataService metadataService;
-
-    @Override
-    public String name() {
-        return NAME;
-    }
-
-    @Override
-    public Object execute(Map<String, Object> input) {
-        String clusterId = (String) input.get("cluster");
-        String search = (String) input.get("search");
-        PageResult<ConsumerGroupVO> page = metadataService.listConsumerGroupsPage(
-                null, clusterId, search, ToolListPagination.page(input), ToolListPagination.pageSize(input));
-        return ToolListPagination.pagedResult(page, page.getItems().stream()
-                .map(ConsumerGroupListToolHandler::safeProjection)
-                .toList());
-    }
-
-    private static Map<String, Object> safeProjection(ConsumerGroupVO group) {
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("name", require(group.getName(), "name"));
-        result.put("namespace", blankIfNull(group.getNamespace()));
-        result.put("clusterId", blankIfNull(group.getClusterId()));
-        result.put("subscriptionMode", requiredEnumName(
-                group.getSubscriptionMode(), "subscriptionMode", group.getName()));
-        result.put("consumeType", requiredEnumName(
-                group.getConsumeType(), "consumeType", group.getName()));
-        result.put("onlineInstances", group.getOnlineInstances());
-        result.put("totalLag", group.getTotalLag() == ConsumerLagResolver.UNKNOWN ? null : group.getTotalLag());
-        result.put("subscribedTopics", copyList(group.getSubscribedTopics()));
-        result.put("retryMaxTimes", group.getRetryMaxTimes());
-        return result;
-    }
-
-    private static String requiredEnumName(Enum<?> value, String field, String groupName) {
-        if (value == null) {
-            throw new IllegalStateException("Consumer group " + field
-                    + " is unavailable: " + groupName);
-        }
-        return value.name();
-    }
-
-    private static String require(String value, String field) {
-        if (value == null || value.isBlank()) {
-            throw new IllegalStateException("Consumer group " + field + " is unavailable");
-        }
-        return value;
-    }
-
-    private static List<String> copyList(List<String> value) {
-        return value == null ? List.of() : List.copyOf(value);
-    }
-
-    private static String blankIfNull(String value) {
-        return value == null ? "" : value;
-    }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/DashboardSummaryToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/DashboardSummaryToolHandler.java
deleted file mode 100644
index 0d8708e7f..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/DashboardSummaryToolHandler.java
+++ /dev/null
@@ -1,111 +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.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.common.exception.BusinessException;
-import org.apache.rocketmq.studio.ops.dashboard.ClusterOverviewVO;
-import org.apache.rocketmq.studio.ops.dashboard.DashboardDataVO;
-import org.apache.rocketmq.studio.ops.dashboard.DashboardService;
-import org.apache.rocketmq.studio.ops.dashboard.DashboardStatsVO;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-@Component
-@RequiredArgsConstructor
-public class DashboardSummaryToolHandler implements ToolHandler {
-
-    private static final String NAME = "rmq.dashboard.summary";
-
-    private final DashboardService dashboardService;
-
-    @Override
-    public String name() {
-        return NAME;
-    }
-
-    @Override
-    public Object execute(Map<String, Object> input) {
-        String clusterId = (String) input.get("cluster");
-        DashboardDataVO dashboard = dashboardService.getDashboard();
-        ClusterOverviewVO cluster = clusters(dashboard).stream()
-                .filter(item -> clusterId.equals(item.getId()))
-                .findFirst()
-                .orElseThrow(() -> new BusinessException(
-                        404, "Dashboard cluster not found: " + clusterId));
-
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("cluster", clusterProjection(cluster));
-        result.put("stats", statsProjection(dashboard.getStats()));
-        return result;
-    }
-
-    private static List<ClusterOverviewVO> clusters(DashboardDataVO dashboard) {
-        if (dashboard == null || dashboard.getClusters() == null) {
-            return List.of();
-        }
-        return dashboard.getClusters();
-    }
-
-    private static Map<String, Object> clusterProjection(ClusterOverviewVO cluster) {
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("id", cluster.getId());
-        result.put("name", cluster.getName());
-        result.put("type", requiredEnumName(cluster.getType(), "type", cluster.getId()));
-        result.put("status", requiredEnumName(
-                cluster.getStatus(), "status", cluster.getId()));
-        result.put("brokers", cluster.getBrokers());
-        result.put("proxies", cluster.getProxies());
-        result.put("topics", cluster.getTopics());
-        result.put("groups", cluster.getGroups());
-        result.put("tpsIn", cluster.getTpsIn());
-        result.put("tpsOut", cluster.getTpsOut());
-        result.put("version", cluster.getVersion());
-        result.put("throughput", cluster.getThroughput() == null
-                ? List.of()
-                : cluster.getThroughput());
-        return result;
-    }
-
-    private static Map<String, Object> statsProjection(DashboardStatsVO stats) {
-        DashboardStatsVO safeStats = stats == null ? new DashboardStatsVO() : stats;
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("totalClusters", safeStats.getTotalClusters());
-        result.put("healthyClusters", safeStats.getHealthyClusters());
-        result.put("totalBrokers", safeStats.getTotalBrokers());
-        result.put("totalProxies", safeStats.getTotalProxies());
-        result.put("totalNameServers", safeStats.getTotalNameServers());
-        result.put("totalTopics", safeStats.getTotalTopics());
-        result.put("totalConsumerGroups", safeStats.getTotalConsumerGroups());
-        result.put("totalMessagesToday", safeStats.getTotalMessagesToday());
-        result.put("messagesPerSecond", safeStats.getMessagesPerSecond());
-        result.put("tpsIn", safeStats.getTpsIn());
-        result.put("tpsOut", safeStats.getTpsOut());
-        return result;
-    }
-
-    private static String requiredEnumName(Enum<?> value, String field, String clusterId) {
-        if (value == null) {
-            throw new IllegalStateException(
-                    "Cluster " + field + " is unavailable: " + clusterId);
-        }
-        return value.name();
-    }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/MessageQueryToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/MessageQueryToolHandler.java
deleted file mode 100644
index 569bbc00b..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/MessageQueryToolHandler.java
+++ /dev/null
@@ -1,127 +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.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.common.exception.BusinessException;
-import org.apache.rocketmq.studio.instance.message.MessageRecordVO;
-import org.apache.rocketmq.studio.instance.message.MessageService;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.math.BigInteger;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * Queries messages in a RocketMQ instance by topic, message id, business key or time range. Read-only
- * and safe for AI/MCP/CLI callers; delegates to the standard message service used by the web UI.
- */
-@Component
-@RequiredArgsConstructor
-public class MessageQueryToolHandler implements ToolHandler {
-
-    private static final String NAME = "rmq.message.query";
-
-    private final MessageService messageService;
-
-    @Override
-    public String name() {
-        return NAME;
-    }
-
-    @Override
-    public Object execute(Map<String, Object> input) {
-        String instanceId = (String) input.get("cluster");
-        String topic = (String) input.get("topic");
-        String msgId = (String) input.get("msgId");
-        String tag = (String) input.get("tag");
-        String key = (String) input.get("key");
-        Long startTime = asLong(input.get("startTime"));
-        Long endTime = asLong(input.get("endTime"));
-        return messageService.queryMessages(instanceId, topic, msgId, tag, key, startTime, endTime).stream()
-                .map(MessageQueryToolHandler::safeProjection)
-                .toList();
-    }
-
-    private static Map<String, Object> safeProjection(MessageRecordVO message) {
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("msgId", require(message.getMsgId(), "msgId"));
-        result.put("topic", blankIfNull(message.getTopic()));
-        result.put("tag", blankIfNull(message.getTag()));
-        result.put("key", blankIfNull(message.getKey()));
-        result.put("storeTime", message.getStoreTime());
-        result.put("storeHost", blankIfNull(message.getStoreHost()));
-        result.put("bornHost", blankIfNull(message.getBornHost()));
-        result.put("body", blankIfNull(message.getBody()));
-        result.put("bodyEncoding", blankIfNull(message.getBodyEncoding()));
-        result.put("bodyTruncated", message.isBodyTruncated());
-        result.put("size", message.getSize());
-        return result;
-    }
-
-    private static Long asLong(Object value) {
-        if (value == null) {
-            return null;
-        }
-        if (value instanceof Number number) {
-            return toEpochMillis(number);
-        }
-        try {
-            return Long.parseLong(value.toString().trim());
-        } catch (NumberFormatException ex) {
-            throw new BusinessException(
-                    400, "startTime and endTime must be integer epoch-milliseconds");
-        }
-    }
-
-    /**
-     * Convert a caller-supplied numeric timestamp to epoch-milliseconds without silent
-     * overflow. Jackson may deliver out-of-range values as {@link BigInteger} (or as a
-     * floating-point number); {@code Number.longValue()} would silently wrap these into a
-     * nonsense, possibly negative, timestamp. Reject values that do not fit instead.
-     */
-    private static long toEpochMillis(Number number) {
-        if (number instanceof BigInteger bigInteger) {
-            try {
-                return bigInteger.longValueExact();
-            } catch (ArithmeticException ex) {
-                throw new BusinessException(
-                        400, "startTime and endTime must fit in a 64-bit epoch-milliseconds value");
-            }
-        }
-        if (number instanceof Double || number instanceof Float) {
-            double doubleValue = number.doubleValue();
-            if (!Double.isFinite(doubleValue) || doubleValue > Long.MAX_VALUE
-                    || doubleValue < Long.MIN_VALUE) {
-                throw new BusinessException(
-                        400, "startTime and endTime must fit in a 64-bit epoch-milliseconds value");
-            }
-        }
-        return number.longValue();
-    }
-
-    private static String require(String value, String field) {
-        if (value == null || value.isBlank()) {
-            throw new IllegalStateException("Message " + field + " is unavailable");
-        }
-        return value;
-    }
-
-    private static String blankIfNull(String value) {
-        return value == null ? "" : value;
-    }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/MessageTraceToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/MessageTraceToolHandler.java
deleted file mode 100644
index ef30a5e45..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/MessageTraceToolHandler.java
+++ /dev/null
@@ -1,89 +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.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.instance.message.ConsumerStatusVO;
-import org.apache.rocketmq.studio.instance.message.MessageService;
-import org.apache.rocketmq.studio.instance.message.TraceNodeVO;
-import org.apache.rocketmq.studio.instance.message.TraceRecordVO;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * Returns the message trace timeline for one message id. Read-only and safe for AI/MCP/CLI callers.
- */
-@Component
-@RequiredArgsConstructor
-public class MessageTraceToolHandler implements ToolHandler {
-
-    private static final String NAME = "rmq.message.trace";
-
-    private final MessageService messageService;
-
-    @Override
-    public String name() {
-        return NAME;
-    }
-
-    @Override
-    public Object execute(Map<String, Object> input) {
-        String instanceId = (String) input.get("cluster");
-        String msgId = (String) input.get("msgId");
-        String topic = (String) input.get("topic");
-        TraceRecordVO trace = messageService.getMessageTrace(instanceId, msgId, topic);
-        return project(msgId, trace);
-    }
-
-    private static Map<String, Object> project(String msgId, TraceRecordVO trace) {
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("msgId", msgId);
-        result.put("nodes", trace.getNodes().stream()
-                .map(MessageTraceToolHandler::projectNode)
-                .toList());
-        result.put("consumerStatus", trace.getConsumerStatus().stream()
-                .map(MessageTraceToolHandler::projectConsumerStatus)
-                .toList());
-        return result;
-    }
-
-    private static Map<String, Object> projectNode(TraceNodeVO node) {
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("title", blankIfNull(node.getTitle()));
-        result.put("timestamp", node.getTimestamp());
-        result.put("status", blankIfNull(node.getStatus()));
-        result.put("costTime", node.getCostTime());
-        result.put("description", blankIfNull(node.getDescription()));
-        return result;
-    }
-
-    private static Map<String, Object> projectConsumerStatus(ConsumerStatusVO status) {
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("group", blankIfNull(status.getGroup()));
-        result.put("deliveryStatus",
-                status.getDeliveryStatus() == null ? "" : status.getDeliveryStatus().name());
-        result.put("consumeTime", status.getConsumeTime());
-        result.put("retryCount", status.getRetryCount());
-        return result;
-    }
-
-    private static String blankIfNull(String value) {
-        return value == null ? "" : value;
-    }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolAccessPolicy.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolAccessPolicy.java
deleted file mode 100644
index b23bfedc7..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolAccessPolicy.java
+++ /dev/null
@@ -1,86 +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.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.auth.AuthenticatedUserContext;
-import org.apache.rocketmq.studio.common.exception.BusinessException;
-import org.springframework.stereotype.Component;
-
-import java.net.URLDecoder;
-import java.nio.charset.StandardCharsets;
-import java.util.Optional;
-import java.util.Set;
-
-@Component
-public class ToolAccessPolicy {
-
-    private static final String TOOL_EXECUTION_PREFIX = "/api/ai/tools/";
-    private static final String TOOL_EXECUTION_SUFFIX = "/execute";
-    /**
-     * These read-labeled tools still let callers steer RocketMQ remoting to user-influenced
-     * broker addresses via msgId-derived lookups, so readers must not gain access before the
-     * address-ownership validation bug is fixed.
-     */
-    private static final Set<String> READER_DENY_LIST = Set.of(
-            "rmq.message.query",
-            "rmq.message.trace");
-
-    private final ToolCatalog catalog;
-
-    public ToolAccessPolicy(ToolCatalog catalog) {
-        this.catalog = catalog;
-    }
-
-    public boolean isToolExecutionPath(String requestPath) {
-        return requestPath != null
-                && requestPath.startsWith(TOOL_EXECUTION_PREFIX)
-                && requestPath.endsWith(TOOL_EXECUTION_SUFFIX);
-    }
-
-    public boolean isReaderAccessiblePath(String requestPath) {
-        return resolveExecutionDefinition(requestPath)
-                .map(this::isReaderAccessible)
-                .orElse(false);
-    }
-
-    public boolean isReaderAccessible(ToolDefinition definition) {
-        return definition != null
-                && definition.isLowRiskReadOnly()
-                && !READER_DENY_LIST.contains(definition.name());
-    }
-
-    public void authorizeCurrentUser(ToolDefinition definition) {
-        if (AuthenticatedUserContext.currentUserIsAdmin() || isReaderAccessible(definition)) {
-            return;
-        }
-        throw new BusinessException(403, "Admin permission required");
-    }
-
-    private Optional<ToolDefinition> resolveExecutionDefinition(String requestPath) {
-        if (!isToolExecutionPath(requestPath)) {
-            return Optional.empty();
-        }
-        String encodedName = requestPath.substring(
-                TOOL_EXECUTION_PREFIX.length(),
-                requestPath.length() - TOOL_EXECUTION_SUFFIX.length());
-        if (encodedName.isBlank()) {
-            return Optional.empty();
-        }
-        String toolName = URLDecoder.decode(encodedName, StandardCharsets.UTF_8);
-        return catalog.find(toolName);
-    }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolDefinition.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolDefinition.java
deleted file mode 100644
index f466ed47f..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolDefinition.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.rocketmq.studio.ops.ai.tool;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-public record ToolDefinition(
-        String name,
-        Cli cli,
-        String description,
-        String riskLevel,
-        String permission,
-        List<String> requiredCapabilities,
-        Map<String, Object> inputSchema,
-        Map<String, Object> outputSchema,
-        String viewHint,
-        boolean deprecated,
-        String replacement) {
-
-    public ToolDefinition {
-        requiredCapabilities = List.copyOf(requiredCapabilities);
-        inputSchema = immutableMap(inputSchema);
-        outputSchema = immutableMap(outputSchema);
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public ToolPermission parsedPermission() {
-        return ToolPermission.parse(permission);
-    }
-
-    public boolean isLowRiskReadOnly() {
-        return "L1".equalsIgnoreCase(riskLevel) && parsedPermission().isReadOnly();
-    }
-
-    private static Map<String, Object> immutableMap(Map<String, Object> source) {
-        Map<String, Object> copy = new LinkedHashMap<>();
-        source.forEach((key, value) -> copy.put(key, immutableValue(value)));
-        return Collections.unmodifiableMap(copy);
-    }
-
-    private static Object immutableValue(Object value) {
-        if (value instanceof Map<?, ?> nestedMap) {
-            Map<String, Object> copy = new LinkedHashMap<>();
-            nestedMap.forEach((key, nestedValue) -> {
-                if (!(key instanceof String stringKey)) {
-                    throw new IllegalArgumentException("JSON Schema keys must be strings");
-                }
-                copy.put(stringKey, immutableValue(nestedValue));
-            });
-            return Collections.unmodifiableMap(copy);
-        }
-        if (value instanceof List<?> nestedList) {
-            List<Object> copy = new ArrayList<>(nestedList.size());
-            nestedList.forEach(item -> copy.add(immutableValue(item)));
-            return Collections.unmodifiableList(copy);
-        }
-        return value;
-    }
-
-    public record Cli(String resource, String verb) {
-    }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolGatewayService.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolGatewayService.java
deleted file mode 100644
index 1f910151a..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolGatewayService.java
+++ /dev/null
@@ -1,252 +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.rocketmq.studio.ops.ai.tool;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.networknt.schema.Error;
-import com.networknt.schema.Schema;
-import com.networknt.schema.SchemaLocation;
-import com.networknt.schema.SchemaRegistry;
-import com.networknt.schema.SpecificationVersion;
-import com.networknt.schema.dialect.Dialects;
-import org.apache.rocketmq.studio.auth.AuthenticatedUserContext;
-import org.apache.rocketmq.studio.common.exception.BusinessException;
-import org.apache.rocketmq.studio.ops.ai.AiToolVO;
-import org.springframework.stereotype.Service;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-@Service
-public class ToolGatewayService {
-
-    private static final String L1 = "L1";
-
-    private final ToolCatalog catalog;
-    private final CapabilityResolver capabilityResolver;
-    private final ObjectMapper objectMapper;
-    private final Map<String, ToolHandler> handlers;
-    private final Map<String, Schema> inputSchemas;
-    private final Map<String, Schema> outputSchemas;
-    private final ToolAccessPolicy toolAccessPolicy;
-
-    public ToolGatewayService(
-            ToolCatalog catalog,
-            CapabilityResolver capabilityResolver,
-            ObjectMapper objectMapper,
-            ToolAccessPolicy toolAccessPolicy,
-            List<ToolHandler> handlers) {
-        this.catalog = catalog;
-        this.capabilityResolver = capabilityResolver;
-        this.objectMapper = objectMapper;
-        this.toolAccessPolicy = toolAccessPolicy;
-        this.handlers = registerHandlers(catalog, handlers);
-
-        SchemaRegistry registry = SchemaRegistry.withDefaultDialect(
-                SpecificationVersion.DRAFT_2020_12);
-        SchemaRegistry metaSchemaRegistry = SchemaRegistry.withDialect(
-                Dialects.getDraft202012());
-        Schema metaSchema = metaSchemaRegistry.getSchema(
-                SchemaLocation.of(Dialects.getDraft202012().getId()));
-        this.inputSchemas = compileSchemas(catalog, registry, metaSchema, true);
-        this.outputSchemas = compileSchemas(catalog, registry, metaSchema, false);
-    }
-
-    public List<AiToolVO> discover(String clusterId) {
-        boolean clusterSelected = clusterId != null && !clusterId.isBlank();
-        DiscoveryCapabilities discoveryCapabilities = resolveDiscoveryCapabilities(clusterId, clusterSelected);
-
-        return catalog.list().stream()
-                .filter(definition -> discoveryCapabilities.clusterCapabilitiesResolved()
-                        || !requiresCluster(definition))
-                .filter(definition -> discoveryCapabilities.capabilities().containsAll(
-                        definition.requiredCapabilities()))
-                .filter(this::isVisibleToCurrentUser)
-                .map(ToolGatewayService::toView)
-                .toList();
-    }
-
-    /**
-     * A tool directory must remain available when the selected cluster cannot report its
-     * architecture. In that case expose only tools that do not require cluster capabilities;
-     * execution still resolves capabilities and returns the original diagnostic error.
-     */
-    private DiscoveryCapabilities resolveDiscoveryCapabilities(String clusterId, boolean clusterSelected) {
-        if (!clusterSelected) {
-            return new DiscoveryCapabilities(false, Collections.emptySet());
-        }
-        try {
-            return new DiscoveryCapabilities(true, Set.copyOf(capabilityResolver.resolve(clusterId)));
-        } catch (BusinessException ignored) {
-            return new DiscoveryCapabilities(false, Collections.emptySet());
-        }
-    }
-
-    private record DiscoveryCapabilities(boolean clusterCapabilitiesResolved, Set<String> capabilities) {
-    }
-
-    public Object execute(String name, Map<String, Object> input) {
-        ToolDefinition definition = catalog.find(name)
-                .orElseThrow(() -> new BusinessException(404, "Tool not found: " + name));
-        ToolHandler handler = handlers.get(name);
-        Map<String, Object> normalizedInput = input == null
-                ? Collections.emptyMap()
-                : input;
-
-        validateInput(definition, normalizedInput);
-        if (!L1.equals(definition.riskLevel())) {
-            throw new BusinessException(
-                    400, "Execution rejected; only L1 tools are enabled: " + name);
-        }
-        toolAccessPolicy.authorizeCurrentUser(definition);
-        enforceCapabilities(definition, normalizedInput);
-
-        Object output = handler.execute(normalizedInput);
-        validateOutput(definition, output);
-        return output;
-    }
-
-    private void validateInput(ToolDefinition definition, Map<String, Object> input) {
-        List<Error> errors = sortedErrors(
-                inputSchemas.get(definition.name()).validate(objectMapper.valueToTree(input)));
-        if (!errors.isEmpty()) {
-            throw new BusinessException(
-                    400,
-                    "Tool input validation failed for " + definition.name() + ": " + errors);
-        }
-    }
-
-    private void enforceCapabilities(
-            ToolDefinition definition,
-            Map<String, Object> input) {
-        if (definition.requiredCapabilities().isEmpty()) {
-            return;
-        }
-        Object cluster = input.get("cluster");
-        if (!(cluster instanceof String clusterId) || clusterId.isBlank()) {
-            throw new BusinessException(
-                    400, "Tool requires a cluster for capability checks: " + definition.name());
-        }
-        Set<String> capabilities = Set.copyOf(capabilityResolver.resolve(clusterId));
-        if (!capabilities.containsAll(definition.requiredCapabilities())) {
-            throw new BusinessException(
-                    400, "Cluster does not support tool: " + definition.name());
-        }
-    }
-
-    private void validateOutput(ToolDefinition definition, Object output) {
-        JsonNode outputNode = objectMapper.valueToTree(output);
-        List<Error> errors = sortedErrors(
-                outputSchemas.get(definition.name()).validate(outputNode));
-        if (!errors.isEmpty()) {
-            throw new IllegalStateException(
-                    "Tool output validation failed for " + definition.name() + ": " + errors);
-        }
-    }
-
-    private static Map<String, ToolHandler> registerHandlers(
-            ToolCatalog catalog,
-            List<ToolHandler> handlers) {
-        Map<String, ToolHandler> registered = new LinkedHashMap<>();
-        for (ToolHandler handler : handlers) {
-            if (registered.putIfAbsent(handler.name(), handler) != null) {
-                throw new IllegalStateException(
-                        "Tool gateway contains duplicate handler: " + handler.name());
-            }
-            if (catalog.find(handler.name()).isEmpty()) {
-                throw new IllegalStateException(
-                        "Tool handler is absent from catalog: " + handler.name());
-            }
-        }
-
-        List<String> missing = catalog.list().stream()
-                .map(ToolDefinition::name)
-                .filter(name -> !registered.containsKey(name))
-                .toList();
-        if (!missing.isEmpty()) {
-            throw new IllegalStateException(
-                    "Tool catalog contains missing handler: " + missing);
-        }
-        return Collections.unmodifiableMap(registered);
-    }
-
-    private Map<String, Schema> compileSchemas(
-            ToolCatalog catalog,
-            SchemaRegistry registry,
-            Schema metaSchema,
-            boolean input) {
-        String schemaKind = input ? "input" : "output";
-        Map<String, Schema> compiled = new LinkedHashMap<>();
-        for (ToolDefinition definition : catalog.list()) {
-            JsonNode schemaNode = objectMapper.valueToTree(
-                    input ? definition.inputSchema() : definition.outputSchema());
-            List<Error> metaSchemaErrors = sortedErrors(metaSchema.validate(schemaNode));
-            if (!metaSchemaErrors.isEmpty()) {
-                throw new IllegalStateException(
-                        "Tool " + schemaKind + " schema is invalid for "
-                                + definition.name() + ": " + metaSchemaErrors);
-            }
-
-            try {
-                Schema schema = registry.getSchema(schemaNode);
-                schema.initializeValidators();
-                compiled.put(definition.name(), schema);
-            } catch (RuntimeException ex) {
-                throw new IllegalStateException(
-                        "Tool " + schemaKind + " schema is invalid for "
-                                + definition.name(), ex);
-            }
-        }
-        return Collections.unmodifiableMap(compiled);
-    }
-
-    private static boolean requiresCluster(ToolDefinition definition) {
-        Object required = definition.inputSchema().get("required");
-        return required instanceof List<?> fields && fields.contains("cluster");
-    }
-
-    private boolean isVisibleToCurrentUser(ToolDefinition definition) {
-        return AuthenticatedUserContext.currentUserIsAdmin() || toolAccessPolicy.isReaderAccessible(definition);
-    }
-
-    private static AiToolVO toView(ToolDefinition definition) {
-        return AiToolVO.builder()
-                .name(definition.name())
-                .description(definition.description())
-                .parameters(definition.inputSchema())
-                .riskLevel(definition.riskLevel())
-                .permission(definition.permission())
-                .requiredCapabilities(definition.requiredCapabilities())
-                .outputSchema(definition.outputSchema())
-                .viewHint(definition.viewHint())
-                .deprecated(definition.deprecated())
-                .replacement(definition.replacement())
-                .build();
-    }
-
-    private static List<Error> sortedErrors(List<Error> errors) {
-        List<Error> sorted = new ArrayList<>(errors);
-        sorted.sort(Comparator.comparing(error -> error.getInstanceLocation().toString()));
-        return sorted;
-    }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolListPagination.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolListPagination.java
deleted file mode 100644
index 70d2930aa..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolListPagination.java
+++ /dev/null
@@ -1,61 +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.rocketmq.studio.ops.ai.tool;
-
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import org.apache.rocketmq.studio.common.domain.PageResult;
-
-/** Shared pagination helpers for list-style AI tool handlers. */
-final class ToolListPagination {
-
-    static final int DEFAULT_PAGE = 1;
-    static final int DEFAULT_PAGE_SIZE = 20;
-
-    private ToolListPagination() {}
-
-    static Map<String, Object> pagedResult(PageResult<?> page, List<Map<String, Object>> items) {
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("items", items);
-        result.put("total", page.getTotal());
-        result.put("page", page.getPage());
-        result.put("size", page.getSize());
-        return result;
-    }
-
-    static int page(Map<String, Object> input) {
-        return optionalPositiveInteger(input.get("page"), DEFAULT_PAGE, "page");
-    }
-
-    static int pageSize(Map<String, Object> input) {
-        return optionalPositiveInteger(input.get("pageSize"), DEFAULT_PAGE_SIZE, "pageSize");
-    }
-
-    private static int optionalPositiveInteger(Object value, int defaultValue, String field) {
-        if (value == null) {
-            return defaultValue;
-        }
-        if (value instanceof Number number) {
-            int result = number.intValue();
-            if (result > 0) {
-                return result;
-            }
-        }
-        throw new IllegalArgumentException(field + " must be positive");
-    }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/TopicListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/TopicListToolHandler.java
deleted file mode 100644
index 50f669f76..000000000
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/TopicListToolHandler.java
+++ /dev/null
@@ -1,85 +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.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.instance.topic.MetadataService;
-import org.apache.rocketmq.studio.instance.topic.TopicVO;
-import org.apache.rocketmq.studio.common.domain.PageResult;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-@Component
-@RequiredArgsConstructor
-public class TopicListToolHandler implements ToolHandler {
-
-    private static final String NAME = "rmq.topic.list";
-
-    private final MetadataService metadataService;
-
-    @Override
-    public String name() {
-        return NAME;
-    }
-
-    @Override
-    public Object execute(Map<String, Object> input) {
-        String clusterId = (String) input.get("cluster");
-        String type = (String) input.get("type");
-        String search = (String) input.get("search");
-        PageResult<TopicVO> page = metadataService.listTopicsPage(
-                null, clusterId, type, search, ToolListPagination.page(input), ToolListPagination.pageSize(input));
-        return ToolListPagination.pagedResult(page, page.getItems().stream()
-                .map(TopicListToolHandler::safeProjection)
-                .toList());
-    }
-
-    private static Map<String, Object> safeProjection(TopicVO topic) {
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("name", require(topic.getName(), "name"));
-        result.put("namespace", blankIfNull(topic.getNamespace()));
-        result.put("clusterId", blankIfNull(topic.getClusterId()));
-        result.put("type", requiredEnumName(topic.getType(), "type", topic.getName()));
-        result.put("writeQueues", topic.getWriteQueues());
-        result.put("readQueues", topic.getReadQueues());
-        result.put("perm", requiredEnumName(topic.getPerm(), "perm", topic.getName()));
-        result.put("messageCount", topic.getMessageCount());
-        result.put("tps", topic.getTps());
-        result.put("consumerGroupCount", topic.getConsumerGroupCount());
-        return result;
-    }
-
-    private static String requiredEnumName(Enum<?> value, String field, String topicName) {
-        if (value == null) {
-            throw new IllegalStateException("Topic " + field + " is unavailable: " + topicName);
-        }
-        return value.name();
-    }
-
-    private static String require(String value, String field) {
-        if (value == null || value.isBlank()) {
-            throw new IllegalStateException("Topic " + field + " is unavailable");
-        }
-        return value;
-    }
-
-    private static String blankIfNull(String value) {
-        return value == null ? "" : value;
-    }
-}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/catalog/CapabilityResolver.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/catalog/CapabilityResolver.java
new file mode 100644
index 000000000..4dd231836
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/catalog/CapabilityResolver.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.rocketmq.studio.ops.ai.tool.catalog;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceType;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.instance.InstanceResolver;
+import org.apache.rocketmq.studio.instance.InstanceVO;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.apache.rocketmq.studio.provider.InstanceCapability;
+import org.apache.rocketmq.studio.provider.InstanceProviderRegistry;
+import org.springframework.stereotype.Component;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@Component
+@RequiredArgsConstructor
+public class CapabilityResolver {
+
+    private final InstanceProviderRegistry providerRegistry;
+    private final InstanceResolver instanceResolver;
+
+    public Set<String> resolve(String cluster) {
+        if (cluster == null || cluster.isBlank()) {
+            throw ToolError.CAPABILITY_CLUSTER_REQUIRED.exception();
+        }
+        InstanceVO instance = instanceResolver.findByName(cluster)
+                .orElseThrow(() -> ToolError.INSTANCE_NOT_FOUND.exception(cluster));
+        return resolve(instance);
+    }
+
+    public Set<String> resolve(InstanceVO instance) {
+        InstanceVendor vendor = instance.getVendor() == null
+                ? InstanceVendor.APACHE : instance.getVendor();
+        InstanceType type = instance.getType() == null ? InstanceType.DIRECT : instance.getType();
+        Set<String> accessCapabilities = switch (type) {
+            case CLOUD -> Set.of("CLOUD_API");
+            case DIRECT -> Set.of(
+                    "BROKER_ADMIN",
+                    "CLUSTER_TOPOLOGY",
+                    "NAMESERVER_ADMIN",
+                    "REMOTING");
+            case PROXY_LOCAL, PROXY_CLUSTER -> Set.of(
+                    "PROXY_DISCOVERY",
+                    "BROKER_ADMIN",
+                    "CLUSTER_TOPOLOGY",
+                    "NAMESERVER_ADMIN",
+                    "REMOTING");
+        };
+        Set<String> capabilities = new HashSet<>(accessCapabilities);
+        providerRegistry.forVendor(vendor).capabilities().stream()
+                .map(InstanceCapability::name)
+                .forEach(capabilities::add);
+        return Set.copyOf(capabilities);
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolCatalog.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/catalog/ToolCatalog.java
similarity index 54%
rename from server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolCatalog.java
rename to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/catalog/ToolCatalog.java
index d33d2a490..14152b918 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/ToolCatalog.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/catalog/ToolCatalog.java
@@ -14,8 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.ops.ai.tool;
+package org.apache.rocketmq.studio.ops.ai.tool.catalog;
 
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
 import com.networknt.schema.Error;
@@ -24,18 +25,20 @@ import com.networknt.schema.Schema;
 import com.networknt.schema.SchemaRegistry;
 import com.networknt.schema.SpecificationVersion;
 import lombok.Getter;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolDefinition;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.io.Resource;
 import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternUtils;
 import org.springframework.stereotype.Component;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Comparator;
-import java.util.HexFormat;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -44,79 +47,87 @@ import java.util.Optional;
 @Component
 public class ToolCatalog {
 
-    static final String CATALOG_RESOURCE = "classpath:tool-catalog/rmq-tools.yaml";
+    static final String MANIFEST_RESOURCE = "classpath:tool-catalog/manifest.yaml";
+    static final String SHARD_PATTERN = "classpath*:tool-catalog/tools/*.yaml";
     static final String SCHEMA_RESOURCE = "classpath:tool-catalog/rmq-tools.schema.json";
 
-    private static final String CLUSTER_LIST_TOOL = "rmq.cluster.list";
     private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory());
 
     @Getter
     private final String version;
-    @Getter
-    private final String minimumClientVersion;
-    @Getter
-    private final String digest;
     private final List<ToolDefinition> definitions;
     private final Map<String, ToolDefinition> definitionsByName;
 
     @Autowired
     public ToolCatalog(ResourceLoader resourceLoader) {
-        ToolCatalog loaded = load(
-                resourceLoader.getResource(CATALOG_RESOURCE),
-                resourceLoader.getResource(SCHEMA_RESOURCE));
+        ToolCatalog loaded = load(resourceLoader);
         this.version = loaded.version;
-        this.minimumClientVersion = loaded.minimumClientVersion;
-        this.digest = loaded.digest;
         this.definitions = loaded.definitions;
         this.definitionsByName = loaded.definitionsByName;
     }
 
     private ToolCatalog(
             String version,
-            String minimumClientVersion,
-            String digest,
             List<ToolDefinition> definitions,
             Map<String, ToolDefinition> definitionsByName) {
         this.version = version;
-        this.minimumClientVersion = minimumClientVersion;
-        this.digest = digest;
         this.definitions = definitions;
         this.definitionsByName = definitionsByName;
     }
 
-    static ToolCatalog load(Resource catalogResource, Resource schemaResource) {
+    static ToolCatalog load(ResourceLoader resourceLoader) {
         try {
-            byte[] catalogBytes = catalogResource.getContentAsByteArray();
-            String catalogYaml = new String(catalogBytes, StandardCharsets.UTF_8);
-            String schemaJson = schemaResource.getContentAsString(StandardCharsets.UTF_8);
+            Resource manifestResource = resourceLoader.getResource(MANIFEST_RESOURCE);
+            Resource schemaResource = resourceLoader.getResource(SCHEMA_RESOURCE);
+            byte[] manifestBytes = manifestResource.getContentAsByteArray();
+            ManifestDocument manifest = YAML_MAPPER.readValue(manifestBytes, ManifestDocument.class);
+
+            ResourcePatternResolver resourcePatternResolver =
+                    ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
+            Resource[] shards = resourcePatternResolver.getResources(SHARD_PATTERN);
+            List<Resource> shardResources = Arrays.stream(shards)
+                    .sorted(Comparator.comparing(Resource::getFilename, Comparator.nullsFirst(Comparator.naturalOrder())))
+                    .toList();
+            if (shardResources.isEmpty()) {
+                throw new IllegalStateException("Tool catalog contains no shards");
+            }
 
+            String schemaJson = schemaResource.getContentAsString(StandardCharsets.UTF_8);
             SchemaRegistry registry = SchemaRegistry.withDefaultDialect(
                     SpecificationVersion.DRAFT_2020_12);
             Schema schema = registry.getSchema(schemaJson, InputFormat.JSON);
-            List<Error> validationErrors = new ArrayList<>(
-                    schema.validate(catalogYaml, InputFormat.YAML));
-            if (!validationErrors.isEmpty()) {
-                validationErrors.sort(Comparator.comparing(
-                        error -> error.getInstanceLocation().toString()));
-                throw new IllegalStateException(
-                        "Tool catalog schema validation failed: " + validationErrors);
+
+            List<ToolDefinition> allTools = new ArrayList<>();
+            for (Resource shard : shardResources) {
+                byte[] shardBytes = shard.getContentAsByteArray();
+                String shardYaml = new String(shardBytes, StandardCharsets.UTF_8);
+                List<Error> shardErrors = new ArrayList<>(
+                        schema.validate(shardYaml, InputFormat.YAML));
+                if (!shardErrors.isEmpty()) {
+                    shardErrors.sort(Comparator.comparing(
+                            error -> error.getInstanceLocation().toString()));
+                    throw new IllegalStateException(
+                            "Tool catalog shard validation failed for " + shard.getFilename()
+                                    + ": " + shardErrors);
+                }
+                ShardDocument shardDoc = YAML_MAPPER.readValue(shardBytes, ShardDocument.class);
+                if (!manifest.version().equals(shardDoc.version())) {
+                    throw new IllegalStateException(
+                            "Tool catalog shard " + shard.getFilename()
+                                    + " version mismatch: expected " + manifest.version()
+                                    + ", got " + shardDoc.version());
+                }
+                allTools.addAll(shardDoc.tools());
             }
 
-            CatalogDocument document = YAML_MAPPER.readValue(catalogBytes, CatalogDocument.class);
-            return validatedCatalog(document, sha256(catalogBytes));
+            CatalogDocument document = new CatalogDocument(manifest.version(), allTools);
+            return validatedCatalog(document);
         } catch (IOException e) {
             throw new IllegalStateException("Unable to load RocketMQ tool catalog", e);
         }
     }
 
-    private static ToolCatalog validatedCatalog(CatalogDocument document, String digest) {
-        int catalogMajor = majorVersion(document.version());
-        int minimumClientMajor = majorVersion(document.minimumClientVersion());
-        if (catalogMajor != minimumClientMajor) {
-            throw new IllegalStateException(
-                    "Catalog and minimum client major versions must match");
-        }
-
+    private static ToolCatalog validatedCatalog(CatalogDocument document) {
         Map<String, ToolDefinition> byName = new LinkedHashMap<>();
         for (ToolDefinition definition : document.tools()) {
             if (byName.putIfAbsent(definition.name(), definition) != null) {
@@ -129,47 +140,48 @@ public class ToolCatalog {
         List<ToolDefinition> immutableDefinitions = List.copyOf(byName.values());
         return new ToolCatalog(
                 document.version(),
-                document.minimumClientVersion(),
-                digest,
                 immutableDefinitions,
                 Map.copyOf(byName));
     }
 
     private static void validateClusterConvention(ToolDefinition definition) {
-        if (CLUSTER_LIST_TOOL.equals(definition.name())) {
-            return;
-        }
         Object required = definition.inputSchema().get("required");
         if (!(required instanceof List<?> requiredFields) || !requiredFields.contains("cluster")) {
             throw new IllegalStateException(
-                    "Remote tool must require cluster: " + definition.name());
+                    "Tool must require cluster: " + definition.name());
         }
     }
 
-    private static int majorVersion(String version) {
-        return Integer.parseInt(version.substring(0, version.indexOf('.')));
+    public List<ToolDefinition> list() {
+        return definitions;
     }
 
-    private static String sha256(byte[] bytes) {
-        try {
-            MessageDigest digest = MessageDigest.getInstance("SHA-256");
-            return HexFormat.of().formatHex(digest.digest(bytes));
-        } catch (NoSuchAlgorithmException e) {
-            throw new IllegalStateException("SHA-256 is unavailable", e);
+    public Optional<ToolDefinition> find(String name) {
+        return Optional.ofNullable(definitionsByName.get(name));
+    }
+
+    public ToolDefinition getDefinition(String name) {
+        if (name == null || name.isBlank()) {
+            throw ToolError.TOOL_NAME_REQUIRED.exception();
+        }
+        ToolDefinition definition = definitionsByName.get(name);
+        if (definition == null) {
+            throw ToolError.TOOL_NOT_FOUND.exception(name);
         }
+        return definition;
     }
 
-    public List<ToolDefinition> list() {
-        return definitions;
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    private record ManifestDocument(String version) {
     }
 
-    public Optional<ToolDefinition> find(String name) {
-        return Optional.ofNullable(definitionsByName.get(name));
+    private record ShardDocument(
+            String version,
+            List<ToolDefinition> tools) {
     }
 
     private record CatalogDocument(
             String version,
-            String minimumClientVersion,
             List<ToolDefinition> tools) {
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclListInput.java
similarity index 64%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclListInput.java
index 2fcbb55d6..ad7600051 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclListInput.java
@@ -14,16 +14,22 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.acl;
 
-import lombok.Getter;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.PageRequest;
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
+public record AclListInput(
+        String cluster,
+        String principal,
+        String resource,
+        String scope,
+        String decision,
+        String aclVersion,
+        PageRequest page) {
 
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
+    public AclListInput {
+        if (page == null) {
+            page = new PageRequest(0, 0);
+        }
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclMutationInput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclMutationInput.java
new file mode 100644
index 000000000..2c3ca71ae
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclMutationInput.java
@@ -0,0 +1,62 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.acl;
+
+import org.apache.rocketmq.studio.instance.acl.AclRuleVO;
+
+import java.util.List;
+
+public record AclMutationInput(
+        String cluster,
+        String id,
+        String principal,
+        String resource,
+        String resourceType,
+        String resourcePattern,
+        List<String> actions,
+        String decision,
+        String scope,
+        String aclVersion) {
+
+    public AclRuleVO toRule(Long ruleId) {
+        return AclRuleVO.builder()
+                .id(ruleId)
+                .principal(principal)
+                .resource(resource)
+                .resourceType(resourceType)
+                .resourcePattern(resourcePattern)
+                .actions(actions)
+                .decision(decision)
+                .scope(scope)
+                .aclVersion(aclVersion)
+                .build();
+    }
+
+    public static AclMutationInput from(AclRuleVO rule) {
+        return new AclMutationInput(
+                null,
+                rule.getId() == null ? null : rule.getId().toString(),
+                rule.getPrincipal(),
+                rule.getResource(),
+                rule.getResourceType(),
+                rule.getResourcePattern(),
+                rule.getActions(),
+                rule.getDecision(),
+                rule.getScope(),
+                rule.getAclVersion());
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclRuleItem.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclRuleItem.java
new file mode 100644
index 000000000..c49eb5dc6
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclRuleItem.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.rocketmq.studio.ops.ai.tool.contract.acl;
+
+import org.apache.rocketmq.studio.instance.acl.AclRuleVO;
+
+import java.util.List;
+
+public record AclRuleItem(
+        Long id,
+        String principal,
+        String resource,
+        String resourceType,
+        String resourcePattern,
+        List<String> actions,
+        String decision,
+        String scope,
+        String aclVersion,
+        String gmtCreate) {
+
+    public static AclRuleItem from(AclRuleVO rule) {
+        return new AclRuleItem(
+                rule.getId(),
+                rule.getPrincipal(),
+                rule.getResource(),
+                rule.getResourceType(),
+                rule.getResourcePattern(),
+                rule.getActions(),
+                rule.getDecision(),
+                rule.getScope(),
+                rule.getAclVersion(),
+                rule.getGmtCreate() == null ? null : rule.getGmtCreate().toString());
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclUserItem.java
similarity index 58%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclUserItem.java
index 141b1cef2..5fcf60be6 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/AclUserItem.java
@@ -14,26 +14,25 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.cluster.broker;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.acl;
 
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.acl.AclUserVO;
 
 import java.util.List;
 
-public interface ClusterProvider {
-
-    List<ClusterVO> discoverClusters();
-
-    default List<ClusterVO> discoverClusters(String instanceId) {
-        return discoverClusters();
-    }
-
-    default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
-        return List.of();
-    }
-
-    ClusterVO refreshClusterDetail(String clusterId);
-
-    default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
-        return refreshClusterDetail(clusterId);
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record AclUserItem(
+        String id,
+        String username,
+        boolean admin,
+        List<String> clusters) {
+
+    public static AclUserItem from(AclUserVO user) {
+        return new AclUserItem(
+                user.getId() == null ? null : user.getId().toString(),
+                user.getUsername(),
+                user.isAdmin(),
+                user.getClusters());
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/UserCreateInput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/UserCreateInput.java
index 2fcbb55d6..191869020 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/acl/UserCreateInput.java
@@ -14,16 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.acl;
 
-import lombok.Getter;
+import java.util.List;
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record UserCreateInput(
+        String cluster,
+        String username,
+        Boolean admin,
+        List<String> clusters) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/alert/AlertRuleListInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/alert/AlertRuleListInput.java
index 2fcbb55d6..8c7301805 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/alert/AlertRuleListInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.alert;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record AlertRuleListInput(String cluster, String search, Boolean enabled) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/alert/AlertRuleListItem.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/alert/AlertRuleListItem.java
new file mode 100644
index 000000000..203e187e2
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/alert/AlertRuleListItem.java
@@ -0,0 +1,54 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.alert;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.ops.alert.AlertRuleVO;
+
+import java.util.List;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record AlertRuleListItem(
+        Long id,
+        String name,
+        String metric,
+        String operator,
+        double threshold,
+        String thresholdUnit,
+        String duration,
+        List<String> channels,
+        boolean enabled,
+        String description) {
+
+    public AlertRuleListItem {
+        channels = channels == null ? List.of() : channels;
+    }
+
+    public static AlertRuleListItem from(AlertRuleVO rule) {
+        return new AlertRuleListItem(
+                rule.getId(),
+                rule.getName(),
+                rule.getMetric(),
+                rule.getOperator(),
+                rule.getThreshold(),
+                rule.getThresholdUnit(),
+                rule.getDuration(),
+                rule.getChannels(),
+                rule.isEnabled(),
+                rule.getDescription());
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigOutput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigOutput.java
new file mode 100644
index 000000000..8d08b5c15
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigOutput.java
@@ -0,0 +1,98 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.broker;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.cluster.config.BrokerConfigDiffVO;
+
+import java.util.List;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record BrokerConfigOutput(
+        String cluster,
+        boolean complete,
+        boolean driftDetected,
+        int brokerCount,
+        int reachableBrokerCount,
+        List<String> comparedFields,
+        List<BrokerStatus> brokers,
+        List<ConfigDifference> differences) {
+
+    public static BrokerConfigOutput from(BrokerConfigDiffVO diff, String instanceId) {
+        return new BrokerConfigOutput(
+                instanceId,
+                diff.isComplete(),
+                diff.isDriftDetected(),
+                diff.getBrokerCount(),
+                diff.getReachableBrokerCount(),
+                diff.getComparedFields(),
+                diff.getBrokers() != null
+                        ? diff.getBrokers().stream().map(BrokerStatus::from).toList()
+                        : List.of(),
+                diff.getDifferences() != null
+                        ? diff.getDifferences().stream().map(ConfigDifference::from).toList()
+                        : List.of());
+    }
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    public record BrokerStatus(
+            String name,
+            String address,
+            boolean reachable,
+            String message) {
+
+        public static BrokerStatus from(BrokerConfigDiffVO.BrokerStatusVO broker) {
+            return new BrokerStatus(
+                    broker.getName(),
+                    broker.getAddress(),
+                    broker.isReachable(),
+                    broker.getMessage());
+        }
+    }
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    public record ConfigDifference(
+            String field,
+            String brokerProperty,
+            List<ConfigValue> values) {
+
+        public static ConfigDifference from(BrokerConfigDiffVO.ConfigDifferenceVO diff) {
+            return new ConfigDifference(
+                    diff.getField(),
+                    diff.getBrokerProperty(),
+                    diff.getValues() != null
+                            ? diff.getValues().stream().map(ConfigValue::from).toList()
+                            : List.of());
+        }
+    }
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    public record ConfigValue(
+            String brokerName,
+            String address,
+            boolean configured,
+            String value) {
+
+        public static ConfigValue from(BrokerConfigDiffVO.ConfigValueVO value) {
+            return new ConfigValue(
+                    value.getBrokerName(),
+                    value.getAddress(),
+                    value.isConfigured(),
+                    value.getValue());
+        }
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigUpdateInput.java
similarity index 66%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigUpdateInput.java
index 2fcbb55d6..591bc0964 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigUpdateInput.java
@@ -14,16 +14,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.broker;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record BrokerConfigUpdateInput(
+        String cluster,
+        String flushDiskType,
+        Boolean autoCreateTopicEnable,
+        Boolean autoCreateSubscriptionGroup,
+        Integer maxMessageSize,
+        Integer fileReservedTime,
+        Integer writeQueueNums,
+        Integer readQueueNums,
+        Integer brokerPermission) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigUpdateOutput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigUpdateOutput.java
new file mode 100644
index 000000000..a6b41a12d
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerConfigUpdateOutput.java
@@ -0,0 +1,49 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.broker;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.cluster.config.BrokerConfigUpdateFailureVO;
+import org.apache.rocketmq.studio.cluster.config.ClusterConfigUpdateResultVO;
+
+import java.util.List;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record BrokerConfigUpdateOutput(
+        String status,
+        List<String> successfulBrokers,
+        List<BrokerConfigUpdateFailure> failedBrokers) {
+
+    public static BrokerConfigUpdateOutput from(ClusterConfigUpdateResultVO result) {
+        return new BrokerConfigUpdateOutput(
+                result.getStatus() != null ? result.getStatus().name() : null,
+                result.getSuccessfulBrokers() != null ? result.getSuccessfulBrokers() : List.of(),
+                result.getFailedBrokers() != null
+                        ? result.getFailedBrokers().stream().map(BrokerConfigUpdateFailure::from).toList()
+                        : List.of());
+    }
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    public record BrokerConfigUpdateFailure(
+            String address,
+            String message) {
+
+        public static BrokerConfigUpdateFailure from(BrokerConfigUpdateFailureVO failure) {
+            return new BrokerConfigUpdateFailure(failure.getAddress(), failure.getMessage());
+        }
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerDescribeInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerDescribeInput.java
index 2fcbb55d6..06c329731 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/broker/BrokerDescribeInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.broker;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record BrokerDescribeInput(String cluster, String broker) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientDescribeInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientDescribeInput.java
index 2fcbb55d6..33c110c72 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientDescribeInput.java
@@ -14,16 +14,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.client;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record ClientDescribeInput(
+        String cluster,
+        String clientId,
+        String type) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientInstance.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientInstance.java
new file mode 100644
index 000000000..1dbadcf26
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientInstance.java
@@ -0,0 +1,44 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.client;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.group.ConsumerInstanceVO;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record ClientInstance(
+        String clientId,
+        String protocol,
+        String address,
+        List<String> subscribedTopics,
+        String lastHeartbeat,
+        Map<String, Long> topicLag) {
+
+    public static ClientInstance from(ConsumerInstanceVO instance) {
+        return new ClientInstance(
+                instance.getClientId(),
+                instance.getProtocol() != null ? instance.getProtocol().name() : "UNKNOWN",
+                instance.getAddress(),
+                Objects.requireNonNullElseGet(instance.getSubscribedTopics(), List::of),
+                instance.getLastHeartbeat() == null ? null : instance.getLastHeartbeat().toString(),
+                Objects.requireNonNullElseGet(instance.getTopicLag(), Map::of));
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientListInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientListInput.java
index 2fcbb55d6..b3dd11140 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/client/ClientListInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.client;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record ClientListInput(String cluster, String type) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/cluster/ClusterListInput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/cluster/ClusterListInput.java
index 2fcbb55d6..e69e9e46d 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/cluster/ClusterListInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.cluster;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record ClusterListInput(String cluster, String status) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/cluster/ClusterListItem.java
similarity index 50%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/cluster/ClusterListItem.java
index 141b1cef2..ab2a9a5b6 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/cluster/ClusterListItem.java
@@ -14,26 +14,27 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.cluster.broker;
-
-
-import java.util.List;
-
-public interface ClusterProvider {
-
-    List<ClusterVO> discoverClusters();
-
-    default List<ClusterVO> discoverClusters(String instanceId) {
-        return discoverClusters();
-    }
-
-    default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
-        return List.of();
-    }
-
-    ClusterVO refreshClusterDetail(String clusterId);
-
-    default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
-        return refreshClusterDetail(clusterId);
+package org.apache.rocketmq.studio.ops.ai.tool.contract.cluster;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.cluster.broker.ClusterVO;
+import org.apache.rocketmq.studio.common.domain.enums.ClusterStatus;
+import org.apache.rocketmq.studio.common.domain.enums.ClusterType;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record ClusterListItem(
+        String id,
+        String name,
+        ClusterType type,
+        ClusterStatus status,
+        String version) {
+
+    public static ClusterListItem from(ClusterVO cluster) {
+        return new ClusterListItem(
+                cluster.getId(),
+                cluster.getName(),
+                cluster.getType(),
+                cluster.getStatus(),
+                cluster.getVersion());
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/BrokerClusterInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/BrokerClusterInput.java
index 2fcbb55d6..ebb2d4124 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/BrokerClusterInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record BrokerClusterInput(String cluster) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/CapabilitiesOutput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/CapabilitiesOutput.java
index 2fcbb55d6..d7cbca6e5 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/CapabilitiesOutput.java
@@ -14,16 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
 
-import lombok.Getter;
+import java.util.List;
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record CapabilitiesOutput(
+        String cluster,
+        List<String> capabilities) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ClusterInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ClusterInput.java
index 2fcbb55d6..94edc66ff 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ClusterInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record ClusterInput(String cluster) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ClusterListOutput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ClusterListOutput.java
index 2fcbb55d6..63f80c109 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ClusterListOutput.java
@@ -14,16 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
 
-import lombok.Getter;
+import java.util.List;
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record ClusterListOutput<T>(String cluster, List<T> items) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ListOutput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ListOutput.java
index 2fcbb55d6..1b1cb6b4c 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ListOutput.java
@@ -14,16 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
 
-import lombok.Getter;
+import java.util.List;
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record ListOutput<T>(List<T> items) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/MutationOutput.java
similarity index 60%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/MutationOutput.java
index 2fcbb55d6..e18ec895a 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/MutationOutput.java
@@ -14,16 +14,22 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
 
-import lombok.Getter;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record MutationOutput<R>(
+        Status status,
+        String cluster,
+        ToolPlan plan,
+        @JsonProperty("confirm_token") String confirmToken,
+        R result) {
 
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
+    public enum Status {
+        PLANNED,
+        EXECUTED
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/PageOutput.java
similarity index 60%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/PageOutput.java
index 141b1cef2..6536ad232 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/PageOutput.java
@@ -14,26 +14,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.cluster.broker;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
 
+import org.apache.rocketmq.studio.common.domain.PageResult;
 
 import java.util.List;
-
-public interface ClusterProvider {
-
-    List<ClusterVO> discoverClusters();
-
-    default List<ClusterVO> discoverClusters(String instanceId) {
-        return discoverClusters();
-    }
-
-    default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
-        return List.of();
-    }
-
-    ClusterVO refreshClusterDetail(String clusterId);
-
-    default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
-        return refreshClusterDetail(clusterId);
+import java.util.function.Function;
+
+public record PageOutput<T>(
+        int page,
+        int pageSize,
+        long total,
+        List<T> items) {
+
+    public static <S, T> PageOutput<T> from(PageResult<S> page, Function<S, T> mapper) {
+        return new PageOutput<>(
+                page.getPage(),
+                page.getSize(),
+                page.getTotal(),
+                page.getItems().stream().map(mapper).toList());
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/PageRequest.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/PageRequest.java
index 2fcbb55d6..43cf22221 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/PageRequest.java
@@ -14,16 +14,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
 
-import lombok.Getter;
+public record PageRequest(int page, int pageSize) {
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
+    public PageRequest {
+        if (page <= 0) {
+            page = 1;
+        }
+        if (pageSize <= 0) {
+            pageSize = 20;
+        }
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ResourceDeleteInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ResourceDeleteInput.java
index 2fcbb55d6..64938f65a 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/ResourceDeleteInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record ResourceDeleteInput(String cluster, String id) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/TimeRange.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/TimeRange.java
index 2fcbb55d6..f1c7fd642 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/common/TimeRange.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.common;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record TimeRange(Long startTime, Long endTime) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQClearInput.java
similarity index 67%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQClearInput.java
index 2fcbb55d6..86effbf08 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQClearInput.java
@@ -1,6 +1,6 @@
 /*
  * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
+ * 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
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.dlq;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record DLQClearInput(String cluster, String group) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQListInput.java
similarity index 69%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQListInput.java
index 2fcbb55d6..9c4a343b5 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQListInput.java
@@ -14,16 +14,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.dlq;
 
-import lombok.Getter;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.PageRequest;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.TimeRange;
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record DLQListInput(
+        String cluster,
+        String group,
+        TimeRange time,
+        PageRequest page,
+        String search) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQListOutput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQListOutput.java
new file mode 100644
index 000000000..318da783c
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQListOutput.java
@@ -0,0 +1,95 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.dlq;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record DLQListOutput(
+        String cluster,
+        String group,
+        Integer page,
+        Integer pageSize,
+        Long total,
+        java.util.List<?> items) {
+
+    public static DLQListOutput ofGroups(
+            String cluster,
+            Integer page,
+            Integer pageSize,
+            Long total,
+            java.util.List<DLQGroupItem> items) {
+        return new DLQListOutput(cluster, null, page, pageSize, total, items);
+    }
+
+    public static DLQListOutput ofMessages(
+            String cluster,
+            String group,
+            Integer page,
+            Integer pageSize,
+            Long total,
+            java.util.List<DLQMessageItem> items) {
+        return new DLQListOutput(cluster, group, page, pageSize, total, items);
+    }
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    public record DLQGroupItem(
+            String groupName,
+            String dlqTopic,
+            long messageCount,
+            int retryCount,
+            String status,
+            boolean statsAvailable,
+            String lastEnqueueTime) {
+
+        public static DLQGroupItem from(
+                org.apache.rocketmq.studio.instance.dlq.DLQGroupVO group) {
+            return new DLQGroupItem(
+                    group.getGroupName(),
+                    group.getDlqTopic(),
+                    group.getMessageCount(),
+                    group.getRetryCount(),
+                    group.getStatus(),
+                    group.isStatsAvailable(),
+                    group.getLastEnqueueTime() != null
+                            ? group.getLastEnqueueTime().toString() : null);
+        }
+    }
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    public record DLQMessageItem(
+            String msgId,
+            String topic,
+            int queueId,
+            long offset,
+            long storeTime,
+            String keys,
+            String body) {
+
+        public static DLQMessageItem from(
+                org.apache.rocketmq.studio.instance.dlq.DLQMessageVO message) {
+            return new DLQMessageItem(
+                    message.getMsgId(),
+                    message.getTopic(),
+                    message.getQueueId(),
+                    message.getOffset(),
+                    message.getStoreTime(),
+                    message.getKeys(),
+                    message.getBody());
+        }
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQResendInput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQResendInput.java
index 2fcbb55d6..aa8a37008 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQResendInput.java
@@ -14,16 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.dlq;
 
-import lombok.Getter;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.TimeRange;
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record DLQResendInput(
+        String cluster,
+        String group,
+        TimeRange time,
+        String targetTopic) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQResendOutput.java
similarity index 55%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQResendOutput.java
index 141b1cef2..c0e040177 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/dlq/DLQResendOutput.java
@@ -14,26 +14,25 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.cluster.broker;
-
-
-import java.util.List;
-
-public interface ClusterProvider {
-
-    List<ClusterVO> discoverClusters();
-
-    default List<ClusterVO> discoverClusters(String instanceId) {
-        return discoverClusters();
-    }
-
-    default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
-        return List.of();
-    }
-
-    ClusterVO refreshClusterDetail(String clusterId);
-
-    default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
-        return refreshClusterDetail(clusterId);
+package org.apache.rocketmq.studio.ops.ai.tool.contract.dlq;
+
+import org.apache.rocketmq.studio.instance.dlq.DLQResendResultVO;
+
+public record DLQResendOutput(
+        int matched,
+        int resent,
+        int failed,
+        String outcome,
+        boolean scanIncomplete,
+        int failedQueueCount) {
+
+    public static DLQResendOutput from(DLQResendResultVO result) {
+        return new DLQResendOutput(
+                result.getMatched(),
+                result.getResent(),
+                result.getFailed(),
+                result.getOutcome(),
+                result.isScanIncomplete(),
+                result.getFailedQueueCount());
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupClientsOutput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupClientsOutput.java
new file mode 100644
index 000000000..433306e6d
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupClientsOutput.java
@@ -0,0 +1,63 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.group;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+import org.apache.rocketmq.studio.instance.group.ConsumerInstanceVO;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.client.ClientInstance;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record GroupClientsOutput(
+        String cluster,
+        String group,
+        String topic,
+        int totalClients,
+        List<ClientInstance> clients) {
+
+    public static GroupClientsOutput from(
+            ConsumerGroupVO group,
+            String cluster,
+            String requestedGroup,
+            String topic) {
+        List<ClientInstance> clients = group.getInstances() == null ? List.of() : group.getInstances().stream()
+                .filter(java.util.Objects::nonNull)
+                .filter(instance -> matchesTopic(instance, topic))
+                .map(ClientInstance::from)
+                .toList();
+        return new GroupClientsOutput(
+                cluster,
+                StringUtils.hasText(group.getName()) ? group.getName() : requestedGroup,
+                StringUtils.hasText(topic) ? topic : null,
+                clients.size(),
+                clients);
+    }
+
+    static boolean matchesTopic(ConsumerInstanceVO instance, String topic) {
+        if (!StringUtils.hasText(topic)) {
+            return true;
+        }
+        List<String> subscribedTopics = instance.getSubscribedTopics();
+        if (subscribedTopics != null && subscribedTopics.contains(topic)) {
+            return true;
+        }
+        return instance.getTopicLag() != null && instance.getTopicLag().containsKey(topic);
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupDescribeInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupDescribeInput.java
index 2fcbb55d6..ad4925ba6 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupDescribeInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record GroupDescribeInput(String cluster, String group, String namespace) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupDescribeOutput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupDescribeOutput.java
new file mode 100644
index 000000000..8e9f51676
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupDescribeOutput.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.rocketmq.studio.ops.ai.tool.contract.group;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.common.domain.enums.ConsumeType;
+import org.apache.rocketmq.studio.common.domain.enums.SubscriptionMode;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+import org.apache.rocketmq.studio.instance.group.ConsumerInstanceVO;
+import org.apache.rocketmq.studio.instance.group.SubscriptionEntryVO;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.client.ClientInstance;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+import java.util.Objects;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record GroupDescribeOutput(
+        String cluster,
+        String group,
+        String namespace,
+        String instanceId,
+        SubscriptionMode subscriptionMode,
+        ConsumeType consumeType,
+        int onlineInstances,
+        long totalLag,
+        List<String> subscribedTopics,
+        String subscriptionDataType,
+        String deliveryOrderType,
+        Integer retryMaxTimes,
+        int delaySeconds,
+        List<Subscription> subscriptions,
+        List<ClientInstance> instances,
+        Health health,
+        List<GroupListItem> configurations) {
+
+    public static GroupDescribeOutput from(
+            ConsumerGroupVO source,
+            String cluster,
+            String requestedGroup,
+            List<SubscriptionEntryVO> subscriptionEntries,
+            Health health) {
+        return from(source, cluster, requestedGroup, subscriptionEntries, health, List.of(source));
+    }
+
+    public static GroupDescribeOutput from(ConsumerGroupVO source, String cluster, String requestedGroup,
+                                            List<SubscriptionEntryVO> subscriptionEntries, Health health,
+                                            List<ConsumerGroupVO> configurations) {
+        List<ConsumerInstanceVO> sourceInstances = source.getInstances();
+        List<ClientInstance> clients = sourceInstances == null
+                ? List.of()
+                : sourceInstances.stream()
+                        .filter(Objects::nonNull)
+                        .map(ClientInstance::from)
+                        .toList();
+        List<String> subscribedTopics = source.getSubscribedTopics();
+        List<SubscriptionEntryVO> entries = subscriptionEntries == null
+                ? List.of()
+                : subscriptionEntries;
+        return new GroupDescribeOutput(
+                cluster,
+                StringUtils.hasText(source.getName()) ? source.getName() : requestedGroup,
+                source.getNamespace(),
+                source.getInstanceId(),
+                source.getSubscriptionMode(),
+                source.getConsumeType(),
+                source.getOnlineInstances(),
+                source.getTotalLag(),
+                subscribedTopics == null ? List.of() : subscribedTopics,
+                source.getSubscriptionDataType(),
+                source.getDeliveryOrderType(),
+                configurations.size() == 1 ? source.getRetryMaxTimes() : null,
+                source.getDelaySeconds(),
+                entries.stream()
+                        .filter(Objects::nonNull)
+                        .map(Subscription::from)
+                        .toList(),
+                clients,
+                health,
+                configurations.stream().map(GroupListItem::from).toList());
+    }
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    public record Subscription(
+            String topic,
+            String expression,
+            String type,
+            String filterMode,
+            String consistency) {
+
+        static Subscription from(SubscriptionEntryVO source) {
+            return new Subscription(
+                    source.getTopic(),
+                    source.getExpression(),
+                    source.getType(),
+                    source.getFilterMode(),
+                    source.getConsistency());
+        }
+    }
+
+    public record Health(String status, List<String> reasons) {
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupInput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupInput.java
new file mode 100644
index 000000000..00de33ba3
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupInput.java
@@ -0,0 +1,78 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.group;
+
+import org.apache.rocketmq.studio.common.domain.enums.ConsumeType;
+import org.apache.rocketmq.studio.common.domain.enums.SubscriptionMode;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+
+public record GroupInput(
+        String cluster,
+        String group,
+        String namespace,
+        SubscriptionMode subscriptionMode,
+        ConsumeType consumeType,
+        String subscriptionDataType,
+        String deliveryOrderType,
+        Integer retryMaxTimes,
+        Integer delaySeconds) {
+
+    public ConsumerGroupVO toConsumerGroupVO() {
+        ConsumerGroupVO vo = new ConsumerGroupVO();
+        vo.setName(group);
+        vo.setNamespace(namespace);
+        vo.setInstanceId(cluster);
+        vo.setSubscriptionDataType(subscriptionDataType);
+        vo.setDeliveryOrderType(deliveryOrderType);
+        if (retryMaxTimes != null) {
+            vo.setRetryMaxTimes(retryMaxTimes);
+        }
+        if (delaySeconds != null) {
+            vo.setDelaySeconds(delaySeconds);
+        }
+        vo.setSubscriptionMode(subscriptionMode);
+        vo.setConsumeType(consumeType);
+        return vo;
+    }
+
+    public ConsumerGroupVO mergeWith(ConsumerGroupVO current) {
+        ConsumerGroupVO merged = new ConsumerGroupVO();
+        org.springframework.beans.BeanUtils.copyProperties(current, merged);
+        if (namespace != null) merged.setNamespace(namespace);
+        if (subscriptionMode != null) merged.setSubscriptionMode(subscriptionMode);
+        if (consumeType != null) merged.setConsumeType(consumeType);
+        if (subscriptionDataType != null) merged.setSubscriptionDataType(subscriptionDataType);
+        if (deliveryOrderType != null) merged.setDeliveryOrderType(deliveryOrderType);
+        if (retryMaxTimes != null) merged.setRetryMaxTimes(retryMaxTimes);
+        if (delaySeconds != null) merged.setDelaySeconds(delaySeconds);
+        return merged;
+    }
+
+    public static GroupInput from(ConsumerGroupVO group) {
+        return new GroupInput(
+                group.getInstanceId(),
+                group.getName(),
+                group.getNamespace(),
+                group.getSubscriptionMode(),
+                group.getConsumeType(),
+                group.getSubscriptionDataType(),
+                group.getDeliveryOrderType(),
+                group.getRetryMaxTimes(),
+                group.getDelaySeconds());
+    }
+
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupListInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupListInput.java
index 2fcbb55d6..771cf6ddb 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupListInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record GroupListInput(String cluster, String search) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupListItem.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupListItem.java
new file mode 100644
index 000000000..1790529b9
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupListItem.java
@@ -0,0 +1,50 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.group;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.common.domain.enums.ConsumeType;
+import org.apache.rocketmq.studio.common.domain.enums.SubscriptionMode;
+import org.apache.rocketmq.studio.instance.group.ConsumerGroupVO;
+
+import java.util.List;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record GroupListItem(
+        String name,
+        String namespace,
+        String clusterId,
+        SubscriptionMode subscriptionMode,
+        ConsumeType consumeType,
+        int retryMaxTimes,
+        int onlineInstances,
+        long totalLag,
+        List<String> subscribedTopics) {
+
+    public static GroupListItem from(ConsumerGroupVO group) {
+        return new GroupListItem(
+                group.getName(),
+                group.getNamespace(),
+                group.getClusterId(),
+                group.getSubscriptionMode(),
+                group.getConsumeType(),
+                group.getRetryMaxTimes(),
+                group.getOnlineInstances(),
+                group.getTotalLag(),
+                group.getSubscribedTopics() != null ? group.getSubscribedTopics() : List.of());
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupProgressOutput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupProgressOutput.java
new file mode 100644
index 000000000..a4719e036
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupProgressOutput.java
@@ -0,0 +1,73 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.group;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.group.QueueProgressVO;
+
+import java.util.List;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record GroupProgressOutput(
+        String cluster,
+        String group,
+        String topic,
+        long totalLag,
+        List<QueueProgress> queues) {
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    public record QueueProgress(
+            String broker,
+            int queueId,
+            long brokerOffset,
+            long consumerOffset,
+            long lag) {
+
+        public static QueueProgress from(QueueProgressVO queue) {
+            if (queue == null) {
+                return null;
+            }
+            return new QueueProgress(
+                    queue.getBroker(),
+                    queue.getQueueId(),
+                    queue.getBrokerOffset(),
+                    queue.getConsumerOffset(),
+                    queue.getDiffTotal());
+        }
+    }
+
+    public static GroupProgressOutput from(String cluster, String group, String topic, List<QueueProgressVO> progress) {
+        List<QueueProgressVO> filtered = progress;
+        if (topic != null && !topic.isBlank()) {
+            filtered = (progress == null ? List.<QueueProgressVO>of() : progress).stream()
+                    .filter(q -> topic.equals(q.getTopic()))
+                    .toList();
+        }
+        List<QueueProgress> queues = (filtered == null ? List.<QueueProgressVO>of() : filtered).stream()
+                .map(QueueProgress::from)
+                .toList();
+        long totalLag = queues.stream().anyMatch(queue -> queue.lag() == -1L)
+                ? -1L
+                : queues.stream().mapToLong(QueueProgress::lag).sum();
+        return new GroupProgressOutput(
+                cluster,
+                group,
+                topic,
+                totalLag,
+                queues);
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupResetOffsetInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupResetOffsetInput.java
index 2fcbb55d6..b85469bbc 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupResetOffsetInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record GroupResetOffsetInput(String cluster, String group, String topic, long timestamp) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupTopicInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupTopicInput.java
index 2fcbb55d6..d6a9caca4 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/GroupTopicInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record GroupTopicInput(String cluster, String group, String topic) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/ResetOffsetOutput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/ResetOffsetOutput.java
index 2fcbb55d6..11f9dd0b0 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/ResetOffsetOutput.java
@@ -14,16 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
 
-import lombok.Getter;
+public record ResetOffsetOutput(String group, String topic, long timestamp, boolean reset) {
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
+    public ResetOffsetOutput(String group, String topic, long timestamp) {
+        this(group, topic, timestamp, true);
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/SkipAccumulatedOutput.java
similarity index 67%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/SkipAccumulatedOutput.java
index 2fcbb55d6..72e9075ec 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/group/SkipAccumulatedOutput.java
@@ -1,6 +1,6 @@
 /*
  * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
+ * 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
@@ -14,16 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.group;
 
-import lombok.Getter;
+import java.util.List;
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record SkipAccumulatedOutput(String group, List<String> topics) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/DlqMessageItem.java
similarity index 51%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/DlqMessageItem.java
index 141b1cef2..3404bc4e1 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/DlqMessageItem.java
@@ -14,26 +14,29 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.cluster.broker;
-
-
-import java.util.List;
-
-public interface ClusterProvider {
-
-    List<ClusterVO> discoverClusters();
-
-    default List<ClusterVO> discoverClusters(String instanceId) {
-        return discoverClusters();
-    }
-
-    default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
-        return List.of();
-    }
-
-    ClusterVO refreshClusterDetail(String clusterId);
-
-    default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
-        return refreshClusterDetail(clusterId);
+package org.apache.rocketmq.studio.ops.ai.tool.contract.message;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.dlq.DLQMessageVO;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record DlqMessageItem(
+        String msgId,
+        String topic,
+        String key,
+        long storeTime,
+        int queueId,
+        long offset,
+        String body) {
+
+    public static DlqMessageItem from(DLQMessageVO message) {
+        return new DlqMessageItem(
+                message.getMsgId(),
+                message.getTopic(),
+                message.getKeys(),
+                message.getStoreTime(),
+                message.getQueueId(),
+                message.getOffset(),
+                message.getBody());
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageItem.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageItem.java
new file mode 100644
index 000000000..e3bca48a1
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageItem.java
@@ -0,0 +1,50 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.message;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.message.MessageRecordVO;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record MessageItem(
+        String msgId,
+        String topic,
+        String tag,
+        String key,
+        long storeTime,
+        String storeHost,
+        String bornHost,
+        String body,
+        String bodyEncoding,
+        boolean bodyTruncated,
+        int size) {
+
+    public static MessageItem from(MessageRecordVO message) {
+        return new MessageItem(
+                message.getMsgId(),
+                message.getTopic(),
+                message.getTag(),
+                message.getKey(),
+                message.getStoreTime(),
+                message.getStoreHost(),
+                message.getBornHost(),
+                message.getBody(),
+                message.getBodyEncoding(),
+                message.isBodyTruncated(),
+                message.getSize());
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByIdInput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByIdInput.java
index 2fcbb55d6..815f098ff 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByIdInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.message;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record MessageQueryByIdInput(String cluster, String msgId, String topic) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByKeyInput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByKeyInput.java
index 2fcbb55d6..053c7bb4e 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByKeyInput.java
@@ -14,16 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.message;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record MessageQueryByKeyInput(
+        String cluster,
+        String key,
+        String topic,
+        Long startTime,
+        Long endTime) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByTopicInput.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByTopicInput.java
index 2fcbb55d6..57a50067c 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageQueryByTopicInput.java
@@ -14,16 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.message;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record MessageQueryByTopicInput(
+        String cluster,
+        String topic,
+        String tag,
+        Long startTime,
+        Long endTime) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageResendInput.java
similarity index 67%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageResendInput.java
index 2fcbb55d6..fb92f5b8e 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageResendInput.java
@@ -1,6 +1,6 @@
 /*
  * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
+ * 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
@@ -14,16 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.message;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record MessageResendInput(
+        String cluster,
+        String msgId,
+        String sourceTopic,
+        String targetTopic) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageResendOutput.java
similarity index 65%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageResendOutput.java
index 2fcbb55d6..48e448619 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageResendOutput.java
@@ -1,6 +1,6 @@
 /*
  * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
+ * 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
@@ -14,16 +14,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.message;
 
-import lombok.Getter;
+import com.fasterxml.jackson.annotation.JsonProperty;
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
+public record MessageResendOutput(
+        String originalMsgId,
+        String newMsgId,
+        String targetTopic) {
 
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
+    @JsonProperty("resent")
+    public boolean resent() {
+        return true;
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageTraceOutput.java
similarity index 58%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageTraceOutput.java
index 141b1cef2..c3695b506 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/message/MessageTraceOutput.java
@@ -14,26 +14,30 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.cluster.broker;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.message;
 
+import com.fasterxml.jackson.annotation.JsonInclude;
 
 import java.util.List;
 
-public interface ClusterProvider {
-
-    List<ClusterVO> discoverClusters();
-
-    default List<ClusterVO> discoverClusters(String instanceId) {
-        return discoverClusters();
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record MessageTraceOutput(
+        String msgId,
+        List<Node> nodes,
+        List<ConsumerStatus> consumerStatus) {
+
+    public record Node(
+            String title,
+            long timestamp,
+            String status,
+            long costTime,
+            String description) {
     }
 
-    default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
-        return List.of();
-    }
-
-    ClusterVO refreshClusterDetail(String clusterId);
-
-    default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
-        return refreshClusterDetail(clusterId);
+    public record ConsumerStatus(
+            String group,
+            String deliveryStatus,
+            long consumeTime,
+            int retryCount) {
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/AuditItem.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/AuditItem.java
new file mode 100644
index 000000000..04e66c082
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/AuditItem.java
@@ -0,0 +1,46 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.ops;
+
+import org.apache.rocketmq.studio.ops.audit.AuditRecordVO;
+
+public record AuditItem(
+        Long id,
+        String timestamp,
+        String operator,
+        String operationType,
+        String resourceType,
+        String target,
+        String clusterId,
+        String detail,
+        String result,
+        String errorMessage) {
+
+    public static AuditItem from(AuditRecordVO record) {
+        return new AuditItem(
+                record.getId(),
+                record.getTimestamp() == null ? null : record.getTimestamp().toString(),
+                record.getOperator(),
+                record.getOperationType(),
+                record.getResourceType(),
+                record.getTarget(),
+                record.getClusterId(),
+                record.getDetail(),
+                record.getResult(),
+                record.getErrorMessage());
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/AuditListInput.java
similarity index 62%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/AuditListInput.java
index 2fcbb55d6..e4f1bfc7d 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/AuditListInput.java
@@ -14,16 +14,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.ops;
 
-import lombok.Getter;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.PageRequest;
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
+public record AuditListInput(
+        String search,
+        String operationType,
+        String resourceType,
+        String cluster,
+        String startDate,
+        String endDate,
+        String result,
+        PageRequest page) {
 
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
+    public AuditListInput {
+        if (page == null) {
+            page = new PageRequest(0, 0);
+        }
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/MetricsQueryInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/MetricsQueryInput.java
index 2fcbb55d6..fb14c9839 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/ops/MetricsQueryInput.java
@@ -14,16 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.ops;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record MetricsQueryInput(
+        String cluster,
+        String metric,
+        String profileId,
+        String semanticMetric,
+        long start,
+        long end,
+        String step) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/plan/PlanDescription.java
similarity index 58%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/plan/PlanDescription.java
index 141b1cef2..c75033934 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/plan/PlanDescription.java
@@ -14,26 +14,22 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.cluster.broker;
-
+package org.apache.rocketmq.studio.ops.ai.tool.contract.plan;
 
 import java.util.List;
 
-public interface ClusterProvider {
-
-    List<ClusterVO> discoverClusters();
+/** Fixed operation text; each preview supplies its own state and computed effects. */
+public record PlanDescription(String summary, List<String> impact, List<String> warnings) {
 
-    default List<ClusterVO> discoverClusters(String instanceId) {
-        return discoverClusters();
+    public PlanDescription {
+        impact = List.copyOf(impact);
+        warnings = List.copyOf(warnings);
     }
 
-    default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
-        return List.of();
-    }
-
-    ClusterVO refreshClusterDetail(String clusterId);
-
-    default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
-        return refreshClusterDetail(clusterId);
+    public ToolPlan.Builder builder(Object... args) {
+        ToolPlan.Builder builder = ToolPlan.builder(summary.formatted(args));
+        impact.forEach(builder::impact);
+        warnings.forEach(builder::warning);
+        return builder;
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/plan/ToolPlan.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/plan/ToolPlan.java
new file mode 100644
index 000000000..baed1d321
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/plan/ToolPlan.java
@@ -0,0 +1,136 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.plan;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public record ToolPlan(
+        String summary,
+        List<String> impact,
+        Map<String, Object> before,
+        Map<String, Object> after,
+        List<String> warnings) {
+
+    private static final ObjectMapper STATE_MAPPER = new ObjectMapper()
+            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+    public ToolPlan {
+        impact = List.copyOf(impact);
+        before = immutableState(before);
+        after = immutableState(after);
+        warnings = List.copyOf(warnings);
+    }
+
+    public ToolPlan withWarning(String warning) {
+        List<String> updated = new ArrayList<>(warnings);
+        updated.add(warning);
+        return new ToolPlan(summary, impact, before, after, updated);
+    }
+
+    public <T> T before(Class<T> stateType) {
+        return STATE_MAPPER.convertValue(before, stateType);
+    }
+
+    public <T> T after(Class<T> stateType) {
+        return STATE_MAPPER.convertValue(after, stateType);
+    }
+
+    public static Builder builder(String summary) {
+        return new Builder(summary);
+    }
+
+    private static <K> Map<K, Object> immutableState(Map<K, ?> state) {
+        Map<K, Object> snapshot = new LinkedHashMap<>();
+        state.forEach((key, value) -> snapshot.put(key, immutableValue(value)));
+        return Collections.unmodifiableMap(snapshot);
+    }
+
+    private static Object immutableValue(Object value) {
+        if (value instanceof Map<?, ?> map) {
+            return immutableState(map);
+        }
+        if (value instanceof List<?> list) {
+            return list.stream().map(ToolPlan::immutableValue).toList();
+        }
+        return value;
+    }
+
+    private static Map<String, Object> objectState(Object value) {
+        if (value == null) {
+            return Map.of();
+        }
+        Map<String, Object> state = STATE_MAPPER.convertValue(
+                value, new TypeReference<LinkedHashMap<String, Object>>() { });
+        state.values().removeIf(Objects::isNull);
+        return state;
+    }
+
+    public static final class Builder {
+
+        private final String summary;
+        private Map<String, Object> before = Map.of();
+        private Map<String, Object> after = Map.of();
+        private final List<String> impact = new ArrayList<>();
+        private final List<String> warnings = new ArrayList<>();
+
+        private Builder(String summary) {
+            this.summary = summary;
+        }
+
+        public Builder before(Object value) {
+            before = objectState(value);
+            return this;
+        }
+
+        public Builder after(Object value) {
+            after = objectState(value);
+            return this;
+        }
+
+        public Builder impact(String value) {
+            impact.add(value);
+            return this;
+        }
+
+        public Builder warning(String value) {
+            warnings.add(value);
+            return this;
+        }
+
+        public Builder warnings(List<String> values) {
+            warnings.addAll(values);
+            return this;
+        }
+
+        public Builder warningIf(boolean condition, String value) {
+            return condition ? warning(value) : this;
+        }
+
+        public ToolPlan build() {
+            return new ToolPlan(summary, impact, before, after, warnings);
+        }
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/proxy/ProxyConfigUpdateInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/proxy/ProxyConfigUpdateInput.java
index 2fcbb55d6..6de5fdeb1 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/proxy/ProxyConfigUpdateInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.proxy;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record ProxyConfigUpdateInput(String cluster, String addr) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/proxy/ProxyConfigUpdateOutput.java
similarity index 69%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/proxy/ProxyConfigUpdateOutput.java
index 2fcbb55d6..28bb8561a 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/proxy/ProxyConfigUpdateOutput.java
@@ -14,16 +14,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.proxy;
 
-import lombok.Getter;
+import com.fasterxml.jackson.annotation.JsonInclude;
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record ProxyConfigUpdateOutput(
+        boolean reloaded,
+        String addr) {
 
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
+    public static ProxyConfigUpdateOutput of(String addr) {
+        return new ProxyConfigUpdateOutput(true, addr);
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/LiteTopicListInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/LiteTopicListInput.java
index 2fcbb55d6..f68bb4bf8 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/LiteTopicListInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record LiteTopicListInput(String cluster, String pattern) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicDescribeInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicDescribeInput.java
index 2fcbb55d6..603f29e29 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicDescribeInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record TopicDescribeInput(String cluster, String topic, String namespace) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicDescribeOutput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicDescribeOutput.java
new file mode 100644
index 000000000..60937d26b
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicDescribeOutput.java
@@ -0,0 +1,126 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.topic;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.common.domain.enums.TopicPerm;
+import org.apache.rocketmq.studio.common.domain.enums.TopicType;
+import org.apache.rocketmq.studio.instance.topic.BrokerRouteVO;
+import org.apache.rocketmq.studio.instance.topic.TopicConsumerVO;
+import org.apache.rocketmq.studio.instance.topic.TopicVO;
+
+import java.util.List;
+import java.util.Map;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record TopicDescribeOutput(
+        String cluster,
+        String name,
+        String namespace,
+        String clusterId,
+        TopicType type,
+        int writeQueues,
+        int readQueues,
+        TopicPerm perm,
+        long messageCount,
+        double tps,
+        int consumerGroupCount,
+        String remark,
+        List<ConsumerGroup> consumerGroups,
+        List<Route> routes) {
+
+    public static TopicDescribeOutput from(
+            TopicVO topic, String cluster,
+            List<TopicConsumerVO> consumers,
+            List<BrokerRouteVO> routes) {
+        List<ConsumerGroup> groups = consumers == null
+                ? List.of()
+                : consumers.stream().map(ConsumerGroup::from).toList();
+        List<Route> routeList = routes == null
+                ? List.of()
+                : routes.stream().map(Route::from).toList();
+        return new TopicDescribeOutput(
+                cluster,
+                topic.getName(),
+                topic.getNamespace(),
+                topic.getClusterId(),
+                topic.getType(),
+                topic.getWriteQueues(),
+                topic.getReadQueues(),
+                topic.getPerm(),
+                topic.getMessageCount(),
+                topic.getTps(),
+                topic.getConsumerGroupCount(),
+                topic.getRemark(),
+                groups,
+                routeList);
+    }
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    public record ConsumerGroup(
+            String group,
+            String consumeType,
+            String messageModel,
+            double consumeTps,
+            long diffTotal,
+            boolean metricsAvailable) {
+
+        static ConsumerGroup from(TopicConsumerVO source) {
+            return new ConsumerGroup(
+                    source.getGroup(),
+                    source.getConsumeType() != null ? source.getConsumeType().name() : null,
+                    source.getMessageModel(),
+                    source.getConsumeTps(),
+                    source.getDiffTotal(),
+                    source.isMetricsAvailable());
+        }
+    }
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    public record Route(
+            String brokerName,
+            String brokerAddr,
+            String masterAddr,
+            Map<Long, String> brokerAddrs,
+            List<Long> brokerIds,
+            int replicaCount,
+            int writeQueues,
+            int readQueues,
+            String perm,
+            int permCode,
+            boolean readable,
+            boolean writable,
+            int topicSysFlag) {
+
+        static Route from(BrokerRouteVO source) {
+            return new Route(
+                    source.getBrokerName(),
+                    source.getBrokerAddr(),
+                    source.getMasterAddr(),
+                    source.getBrokerAddrs(),
+                    source.getBrokerIds(),
+                    source.getReplicaCount(),
+                    source.getWriteQueues(),
+                    source.getReadQueues(),
+                    source.getPerm() != null ? source.getPerm().name() : null,
+                    source.getPermCode(),
+                    source.isReadable(),
+                    source.isWritable(),
+                    source.getTopicSysFlag());
+        }
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicInput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicInput.java
new file mode 100644
index 000000000..665d9e730
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicInput.java
@@ -0,0 +1,62 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.topic;
+
+import org.apache.rocketmq.studio.common.domain.enums.TopicPerm;
+import org.apache.rocketmq.studio.common.domain.enums.TopicType;
+import org.apache.rocketmq.studio.instance.topic.TopicVO;
+
+public record TopicInput(
+        String cluster,
+        String topic,
+        String namespace,
+        TopicType type,
+        Integer writeQueues,
+        Integer readQueues,
+        TopicPerm perm,
+        String remark) {
+
+    private static final int DEFAULT_QUEUE_COUNT = 8;
+    private static final TopicType DEFAULT_TYPE = TopicType.NORMAL;
+    private static final TopicPerm DEFAULT_PERM = TopicPerm.RW;
+
+    public TopicVO toTopicVO() {
+        TopicVO vo = new TopicVO();
+        vo.setName(topic);
+        vo.setInstanceId(cluster);
+        vo.setNamespace(namespace);
+        vo.setRemark(remark);
+        vo.setWriteQueues(writeQueues != null ? writeQueues : DEFAULT_QUEUE_COUNT);
+        vo.setReadQueues(readQueues != null ? readQueues : DEFAULT_QUEUE_COUNT);
+        vo.setType(type != null ? type : DEFAULT_TYPE);
+        vo.setPerm(perm != null ? perm : DEFAULT_PERM);
+        return vo;
+    }
+
+    public static TopicInput from(TopicVO topic) {
+        return new TopicInput(
+                topic.getInstanceId(),
+                topic.getName(),
+                topic.getNamespace(),
+                topic.getType(),
+                topic.getWriteQueues(),
+                topic.getReadQueues(),
+                topic.getPerm(),
+                topic.getRemark());
+    }
+
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicListInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicListInput.java
index 2fcbb55d6..f58e4ca96 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicListInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record TopicListInput(String cluster, String type, String search) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicListItem.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicListItem.java
new file mode 100644
index 000000000..838af6f22
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicListItem.java
@@ -0,0 +1,50 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.topic;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.common.domain.enums.TopicPerm;
+import org.apache.rocketmq.studio.common.domain.enums.TopicType;
+import org.apache.rocketmq.studio.instance.topic.TopicVO;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record TopicListItem(
+        String name,
+        String namespace,
+        String clusterId,
+        TopicType type,
+        int writeQueues,
+        int readQueues,
+        TopicPerm perm,
+        long messageCount,
+        double tps,
+        int consumerGroupCount) {
+
+    public static TopicListItem from(TopicVO topic) {
+        return new TopicListItem(
+                topic.getName(),
+                topic.getNamespace(),
+                topic.getClusterId(),
+                topic.getType(),
+                topic.getWriteQueues(),
+                topic.getReadQueues(),
+                topic.getPerm(),
+                topic.getMessageCount(),
+                topic.getTps(),
+                topic.getConsumerGroupCount());
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicOutput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicOutput.java
new file mode 100644
index 000000000..b2908d8d7
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicOutput.java
@@ -0,0 +1,44 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.topic;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.topic.TopicVO;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record TopicOutput(
+        String name,
+        String namespace,
+        String cluster,
+        String type,
+        int writeQueues,
+        int readQueues,
+        String perm,
+        String remark) {
+
+    public static TopicOutput from(TopicVO topic) {
+        return new TopicOutput(
+                topic.getName(),
+                topic.getNamespace(),
+                topic.getClusterId(),
+                topic.getType() != null ? topic.getType().name() : null,
+                topic.getWriteQueues(),
+                topic.getReadQueues(),
+                topic.getPerm() != null ? topic.getPerm().name() : null,
+                topic.getRemark());
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicRouteInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicRouteInput.java
index 2fcbb55d6..0840ac56d 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicRouteInput.java
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record TopicRouteInput(String cluster, String topic, String namespace) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicRouteItem.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicRouteItem.java
new file mode 100644
index 000000000..9c8befb96
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicRouteItem.java
@@ -0,0 +1,57 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.topic;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.topic.BrokerRouteVO;
+
+import java.util.List;
+import java.util.Map;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record TopicRouteItem(
+        String brokerName,
+        String brokerAddr,
+        String masterAddr,
+        Map<Long, String> brokerAddrs,
+        List<Long> brokerIds,
+        int replicaCount,
+        int writeQueues,
+        int readQueues,
+        String perm,
+        int permCode,
+        boolean readable,
+        boolean writable,
+        int topicSysFlag) {
+
+    public static TopicRouteItem from(BrokerRouteVO source) {
+        return new TopicRouteItem(
+                source.getBrokerName(),
+                source.getBrokerAddr(),
+                source.getMasterAddr(),
+                source.getBrokerAddrs(),
+                source.getBrokerIds(),
+                source.getReplicaCount(),
+                source.getWriteQueues(),
+                source.getReadQueues(),
+                source.getPerm() != null ? source.getPerm().name() : null,
+                source.getPermCode(),
+                source.isReadable(),
+                source.isWritable(),
+                source.getTopicSysFlag());
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicSendInput.java
similarity index 73%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicSendInput.java
index 2fcbb55d6..59341b88c 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicSendInput.java
@@ -14,16 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
 
-import lombok.Getter;
-
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
-
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+public record TopicSendInput(
+        String cluster,
+        String topic,
+        String tag,
+        String key,
+        String body,
+        String properties) {
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicSendOutput.java
similarity index 59%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicSendOutput.java
index 2fcbb55d6..d8c0c267e 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicSendOutput.java
@@ -14,16 +14,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.contract.topic;
 
-import lombok.Getter;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.rocketmq.studio.instance.topic.SendMessageVO;
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record TopicSendOutput(
+        String msgId,
+        String offsetMsgId,
+        long sendTime) {
 
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
+    public static TopicSendOutput from(SendMessageVO source) {
+        return new TopicSendOutput(
+                source.getMsgId(),
+                source.getOffsetMsgId(),
+                source.getSendTime());
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicUpdateInput.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicUpdateInput.java
new file mode 100644
index 000000000..71afab917
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/contract/topic/TopicUpdateInput.java
@@ -0,0 +1,59 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.contract.topic;
+
+import org.apache.rocketmq.studio.common.domain.enums.TopicPerm;
+import org.apache.rocketmq.studio.common.domain.enums.TopicType;
+import org.apache.rocketmq.studio.instance.topic.TopicVO;
+import org.springframework.beans.BeanUtils;
+
+/** Optional values stay absent; the input schema rejects explicit JSON nulls. */
+public record TopicUpdateInput(
+        String cluster,
+        String topic,
+        String namespace,
+        TopicType type,
+        Integer writeQueues,
+        Integer readQueues,
+        TopicPerm perm,
+        String remark) {
+
+    /** Applies supplied values to a copy of the current configuration, without creation defaults. */
+    public TopicVO mergeWith(TopicVO current) {
+        TopicVO merged = new TopicVO();
+        BeanUtils.copyProperties(current, merged);
+        if (namespace != null) {
+            merged.setNamespace(namespace);
+        }
+        if (type != null) {
+            merged.setType(type);
+        }
+        if (writeQueues != null) {
+            merged.setWriteQueues(writeQueues);
+        }
+        if (readQueues != null) {
+            merged.setReadQueues(readQueues);
+        }
+        if (perm != null) {
+            merged.setPerm(perm);
+        }
+        if (remark != null) {
+            merged.setRemark(remark);
+        }
+        return merged;
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolDefinition.java
similarity index 52%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolDefinition.java
index 141b1cef2..a54ae2f82 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolDefinition.java
@@ -14,26 +14,36 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.cluster.broker;
-
+package org.apache.rocketmq.studio.ops.ai.tool.core;
 
 import java.util.List;
-
-public interface ClusterProvider {
-
-    List<ClusterVO> discoverClusters();
-
-    default List<ClusterVO> discoverClusters(String instanceId) {
-        return discoverClusters();
+import java.util.Map;
+
+public record ToolDefinition(
+        String name,
+        Cli cli,
+        String description,
+        ToolRiskLevel riskLevel,
+        String permission,
+        List<String> requiredCapabilities,
+        Map<String, Object> inputSchema,
+        Map<String, Object> outputSchema,
+        String viewHint,
+        boolean deprecated,
+        String replacement) {
+
+    public boolean isLowRiskReadOnly() {
+        return isReadOnly() && permission.endsWith(":read");
     }
 
-    default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
-        return List.of();
+    public boolean isReadOnly() {
+        return riskLevel == ToolRiskLevel.L1;
     }
 
-    ClusterVO refreshClusterDetail(String clusterId);
+    public boolean requiresReason() {
+        return riskLevel == ToolRiskLevel.L3;
+    }
 
-    default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
-        return refreshClusterDetail(clusterId);
+    public record Cli(String resource, String verb) {
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolError.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolError.java
new file mode 100644
index 000000000..74fcebf68
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolError.java
@@ -0,0 +1,198 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.core;
+
+import org.springframework.http.HttpStatus;
+
+/**
+ * Tool error scenarios and their HTTP status, public error code and recovery hint.
+ * Multiple scenarios may share the same public code.
+ */
+public enum ToolError {
+
+    TOKEN_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "UNAVAILABLE",
+            "Tool confirmation tokens are unavailable because studio.ai.token-secret is not configured.",
+            "Configure studio.ai.token-secret or STUDIO_AI_TOKEN_SECRET with a base64-encoded key of at least 32 bytes."),
+
+    L3_DISABLED(HttpStatus.FORBIDDEN, "PERMISSION_DENIED",
+            "L3 tools are disabled by server policy.",
+            "Ask an administrator to enable studio.ai.allow-l3-tools before retrying."),
+
+    L3_APPROVAL_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+            "L3 tool is blocked by default and requires break_glass=true before execution: %s",
+            "Set break_glass=true after reviewing the preview plan."),
+
+    L3_REASON_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+            "Tool requires a non-empty reason before L3 execution: %s",
+            "Provide a non-empty reason that explains the L3 operation."),
+
+    CONFIRMATION_TOKEN_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+            "Tool requires confirm_token from a matching preview: %s",
+            "Run the tool with --dry-run, then retry without --dry-run and with the returned confirm_token."),
+
+    CONFIRMATION_TOKEN_INVALID(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+            "Tool confirm_token is invalid, expired, or does not match the tool, caller, Instance or preview input. Tool: %s",
+            "Run with --dry-run again and retry with its fresh token without changing the operation input."),
+
+    TOOL_NAME_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+            "Tool name is required",
+            "Call tools/list and provide one of the returned tool names."),
+
+    TOOL_NOT_FOUND(HttpStatus.NOT_FOUND, "NOT_FOUND",
+            "Tool not found: %s",
+            "Call tools/list to discover the available tool names."),
+
+    CLUSTER_TYPE_UNAVAILABLE(HttpStatus.BAD_REQUEST, "TOOL_CAPABILITY_UNSUPPORTED",
+            "Cluster type is unavailable: %s",
+            "Refresh the cluster metadata or select a cluster with a known type."),
+
+    CAPABILITY_CLUSTER_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+            "Capability lookup requires a cluster",
+            "Select a cluster and retry the tool call."),
+
+    INSTANCE_NOT_FOUND(HttpStatus.NOT_FOUND, "NOT_FOUND",
+            "Instance not found: %s",
+            "List the configured Instances and select an existing Instance."),
+
+    TOOL_CLUSTER_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+            "cluster must be a non-empty target name",
+            "Set cluster to a registered instance name or a configured physical cluster name."),
+
+    TOOL_TARGET_MISMATCH(HttpStatus.FORBIDDEN, "PERMISSION_DENIED",
+            "cluster does not match the authenticated cluster",
+            "Use the same cluster for authentication and tool arguments."),
+
+    TOOL_CAPABILITY_UNSUPPORTED(HttpStatus.BAD_REQUEST, "TOOL_CAPABILITY_UNSUPPORTED",
+            "Instance target does not support tool: %s. Call rmq.capabilities or select another target.",
+            "Call rmq.capabilities for the selected Instance or select another target."),
+
+    ADMIN_REQUIRED(HttpStatus.FORBIDDEN, "PERMISSION_DENIED",
+            "Admin permission required for tool: %s",
+            "Log in as an admin user, or use a low-risk read-only tool that does not require admin permission."),
+
+    INPUT_VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+            "Tool input validation failed for %s: %s",
+            "Correct the reported fields according to the tool input schema and retry."),
+
+    UNEXPECTED_EXECUTION_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_ERROR",
+            "Tool execution failed unexpectedly.",
+            "Retry once; if the failure persists, contact an administrator."),
+
+    ACL_ID_INVALID(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+            "ACL id must be numeric: %s",
+            "Use the numeric ACL id returned by rmq.acl.list."),
+
+    ROLE_NOT_FOUND(HttpStatus.NOT_FOUND, "NOT_FOUND",
+            "Role not found: %s",
+            "List the roles for the selected Instance and choose an existing role."),
+
+    DLQ_GROUP_NOT_FOUND(HttpStatus.NOT_FOUND, "NOT_FOUND",
+            "DLQ group not found: %s",
+            "Call rmq.dlq.list and select a group with retained dead-letter messages."),
+
+    DLQ_STATISTICS_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "UNAVAILABLE",
+            "DLQ statistics are unavailable for group: %s",
+            "Restore broker connectivity and preview again before clearing the DLQ."),
+
+    OFFSET_RESET_PREVIEW_UNSAFE(HttpStatus.CONFLICT, "CONFLICT",
+            "Consumer offset reset preview is incomplete or unsafe for group: %s",
+            "Inspect the preview warnings and target, correct them, then preview again."),
+
+    RESEND_TARGET_TOPIC_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+            "target topic is required when the source message has no topic",
+            "Provide targetTopic explicitly and retry the preview."),
+
+    PROXY_NOT_FOUND(HttpStatus.NOT_FOUND, "NOT_FOUND",
+            "Proxy not found in cluster %s: %s",
+            "Call rmq.proxy.list and select an address returned for this cluster."),
+
+    MESSAGE_PROPERTIES_INVALID(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+            "properties must be a JSON object string of string key-value pairs",
+            "Provide properties as a JSON string such as {\"key1\":\"value1\"} and retry."),
+
+    TOOL_PAYLOAD_INVALID(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+            "%s",
+            "Correct the tool name or arguments and retry the tool call."),
+
+    TOOL_CALL_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+            "Tool call request is required",
+            "Provide a JSON object containing name and arguments."),
+
+    MCP_AUTHENTICATION_REQUIRED(HttpStatus.UNAUTHORIZED, "UNAUTHENTICATED",
+            "MCP authentication is required",
+            "Configure valid MCP credentials and retry the tool call."),
+
+    MCP_AUTHENTICATION_CONTEXT_MISSING(HttpStatus.UNAUTHORIZED, "UNAUTHENTICATED",
+            "MCP transport authentication context is missing.",
+            "Configure valid MCP credentials and retry the tool call."),
+
+    MCP_AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "UNAUTHENTICATED",
+            "%s",
+            "Configure valid MCP credentials and retry the MCP request."),
+
+    MCP_AUTHENTICATION_INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_ERROR",
+            "MCP authentication failed unexpectedly.",
+            "Retry once; if the failure persists, contact an administrator."),
+
+    REQUEST_BODY_INVALID(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+            "Tool request body is invalid.",
+            "Correct the tool request and retry."),
+
+    REQUEST_PARAMETER_REQUIRED(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+            "Missing required tool parameter: %s",
+            "Correct the tool request and retry."),
+
+    REQUEST_PARAMETER_INVALID(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT",
+            "Invalid tool parameter: %s",
+            "Correct the tool request and retry."),
+
+    EXECUTION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "TOOL_EXECUTION_FAILED",
+            "%s",
+            "Review the failure message; no specific recovery action is available.");
+
+    private final HttpStatus httpStatus;
+    private final String code;
+    private final String messageTemplate;
+    private final String hint;
+
+    ToolError(HttpStatus httpStatus, String code, String messageTemplate, String hint) {
+        this.httpStatus = httpStatus;
+        this.code = code;
+        this.messageTemplate = messageTemplate;
+        this.hint = hint;
+    }
+
+    public ToolExecutionException exception(Object... arguments) {
+        return new ToolExecutionException(httpStatus, code, message(arguments), hint);
+    }
+
+    public HttpStatus httpStatus() {
+        return httpStatus;
+    }
+
+    public String code() {
+        return code;
+    }
+
+    public String message(Object... arguments) {
+        return messageTemplate.formatted(arguments);
+    }
+
+    public String hint() {
+        return hint;
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionContext.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionContext.java
new file mode 100644
index 000000000..f1c73c695
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionContext.java
@@ -0,0 +1,129 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.core;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+public record ToolExecutionContext(
+        String cluster,
+        ToolDefinition definition,
+        Map<String, Object> input,
+        String principal) {
+
+    private static final ObjectMapper INPUT_MAPPER = new ObjectMapper()
+            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+    private static final Set<String> CONTROL_FIELDS = Set.of(
+            "break_glass",
+            "confirm_token",
+            "dry_run",
+            "reason");
+
+    public ToolExecutionContext {
+        input = input == null
+                ? Map.of()
+                : Collections.unmodifiableMap(new LinkedHashMap<>(input));
+    }
+
+    public static ToolExecutionContext of(String cluster, ToolDefinition definition, Map<String, Object> input) {
+        return new ToolExecutionContext(cluster, definition, input, null);
+    }
+
+    public static ToolExecutionContext of(
+            String cluster,
+            ToolDefinition definition,
+            Map<String, Object> input,
+            String principal) {
+        return new ToolExecutionContext(cluster, definition, input, principal);
+    }
+
+    public String operationType() {
+        return definition.cli().verb().replace('-', '_').toUpperCase(Locale.ROOT) + "_" + resourceType();
+    }
+
+    public String resourceType() {
+        return definition.cli().resource().replace('-', '_').toUpperCase(Locale.ROOT);
+    }
+
+    /**
+     * Returns business fields without changing their values.
+     */
+    public Map<String, Object> businessInput() {
+        if (input.isEmpty()) {
+            return Map.of();
+        }
+        Map<String, Object> business = new LinkedHashMap<>(input);
+        CONTROL_FIELDS.forEach(business::remove);
+        return Collections.unmodifiableMap(business);
+    }
+
+    /**
+     * Converts business fields into the tool's input record.
+     */
+    @SuppressWarnings("unchecked")
+    public <I> I convertInput(Class<I> inputType) {
+        Map<String, Object> business = businessInput();
+        if (inputType == Map.class) {
+            return (I) business;
+        }
+        return INPUT_MAPPER.convertValue(business, inputType);
+    }
+
+    /**
+     * Resolves the {@code dry_run} control field from the raw input.
+     */
+    public boolean dryRun() {
+        return booleanInput("dry_run");
+    }
+
+    public boolean breakGlass() {
+        return booleanInput("break_glass");
+    }
+
+    public String reason() {
+        return input.get("reason") instanceof String text ? text : null;
+    }
+
+    public String confirmToken() {
+        return input.get("confirm_token") instanceof String text ? text : null;
+    }
+
+    private boolean booleanInput(String field) {
+        Object value = input.get(field);
+        if (value instanceof Boolean bool) {
+            return bool;
+        }
+        return value instanceof String text && Boolean.parseBoolean(text);
+    }
+
+    /**
+     * Returns the cluster name, or throws when it is missing or blank.
+     */
+    public String requireCluster() {
+        if (cluster == null || cluster.isBlank()) {
+            throw ToolError.TOOL_CLUSTER_REQUIRED.exception();
+        }
+        return cluster;
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionException.java
new file mode 100644
index 000000000..9688c9140
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionException.java
@@ -0,0 +1,62 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.core;
+
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.springframework.http.HttpStatus;
+
+public class ToolExecutionException extends BusinessException {
+
+    private final String errorCode;
+    private final String hint;
+
+    public ToolExecutionException(
+            HttpStatus httpStatus,
+            String errorCode,
+            String message,
+            String hint) {
+        super(httpStatus, message);
+        this.errorCode = errorCode;
+        this.hint = hint;
+    }
+
+    public String getErrorCode() {
+        return errorCode;
+    }
+
+    public String getHint() {
+        return hint;
+    }
+
+    public static ToolExecutionException from(BusinessException exception) {
+        if (exception instanceof ToolExecutionException toolException) {
+            return toolException;
+        }
+        HttpStatus status = HttpStatus.resolve(exception.getCode());
+        if (status == null) {
+            status = HttpStatus.INTERNAL_SERVER_ERROR;
+        }
+        ToolExecutionException translated = new ToolExecutionException(
+                status,
+                ToolError.EXECUTION_FAILED.code(),
+                exception.getMessage(),
+                ToolError.EXECUTION_FAILED.hint());
+        translated.initCause(exception);
+        return translated;
+    }
+
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionResult.java
similarity index 54%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionResult.java
index 141b1cef2..0513c5279 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolExecutionResult.java
@@ -14,26 +14,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.cluster.broker;
-
-
-import java.util.List;
-
-public interface ClusterProvider {
-
-    List<ClusterVO> discoverClusters();
-
-    default List<ClusterVO> discoverClusters(String instanceId) {
-        return discoverClusters();
-    }
-
-    default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
-        return List.of();
-    }
-
-    ClusterVO refreshClusterDetail(String clusterId);
+package org.apache.rocketmq.studio.ops.ai.tool.core;
+
+/**
+ * Carries both the business output and the final context after a filter chain
+ * completes. This replaces the shared {@code AtomicReference} that previously
+ * allowed upstream filters to read the latest context via {@code latestContext()}.
+ * Each filter receives the result from {@code chain.proceed()}, reads the
+ * updated context directly, and returns a new result to its caller.
+ */
+public record ToolExecutionResult(Object output, ToolExecutionContext context) {
 
-    default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
-        return refreshClusterDetail(clusterId);
+    public static ToolExecutionResult of(Object output, ToolExecutionContext context) {
+        return new ToolExecutionResult(output, context);
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolHandler.java
similarity index 74%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolHandler.java
index 2fcbb55d6..2bf8ed224 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolHandler.java
@@ -14,16 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.core;
 
-import lombok.Getter;
+public interface ToolHandler<I, O> {
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
+    String name();
 
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
-    }
+    Class<I> inputType();
+
+    O execute(I input, ToolExecutionContext context);
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolInvocation.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolInvocation.java
new file mode 100644
index 000000000..d4bba4abb
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolInvocation.java
@@ -0,0 +1,49 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.core;
+
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+
+/**
+ * A tool call and its request context. Input conversion is deferred until the
+ * preview or terminal invocation so failures remain inside the validation and audit chain.
+ */
+public record ToolInvocation(ToolExecutionContext context, ToolHandler<?, ?> handler) {
+
+    public Object execute() {
+        return invoke(handler, context);
+    }
+
+    public ToolPlan preview() {
+        if (!(handler instanceof MutationToolHandler<?, ?> mutation)) {
+            throw new IllegalStateException("Mutation tool requires MutationToolHandler: "
+                    + context.definition().name());
+        }
+        return preview(mutation, context);
+    }
+
+    private static <I> ToolPlan preview(MutationToolHandler<I, ?> handler, ToolExecutionContext context) {
+        I input = context.convertInput(handler.inputType());
+        return handler.preview(input, context);
+    }
+
+    private static <I, O> O invoke(ToolHandler<I, O> handler, ToolExecutionContext context) {
+        I input = context.convertInput(handler.inputType());
+        return handler.execute(input, context);
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolRiskLevel.java
similarity index 69%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolRiskLevel.java
index 2fcbb55d6..76678fc4f 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/core/ToolRiskLevel.java
@@ -14,16 +14,22 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.core;
 
-import lombok.Getter;
+public enum ToolRiskLevel {
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
+    L1("L1_READ_ONLY"),
+    L2("L2_MUTATION"),
+    L3("L3_HIGH_RISK");
 
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
+    private final String operationLevel;
+
+    ToolRiskLevel(String operationLevel) {
+        this.operationLevel = operationLevel;
+    }
+
+    public String operationLevel() {
+        return operationLevel;
     }
+
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolAuditFilter.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolAuditFilter.java
new file mode 100644
index 000000000..ed307f1b0
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolAuditFilter.java
@@ -0,0 +1,60 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.filter;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolInvocation;
+import org.apache.rocketmq.studio.ops.audit.AuditService;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class ToolAuditFilter implements ToolExecutionFilter {
+
+    private final AuditService auditService;
+
+    @Override
+    public Type type() {
+        return Type.AUDIT;
+    }
+
+    @Override
+    public Object filter(ToolInvocation invocation, Chain chain) {
+        ToolExecutionContext context = invocation.context();
+        Object output;
+        try {
+            output = chain.proceed(invocation);
+        } catch (Exception exception) {
+            this.record(context, "FAILED", exception.getMessage());
+            throw exception;
+        }
+        this.record(context, "SUCCESS", null);
+        return output;
+    }
+
+    private void record(ToolExecutionContext context, String result, String errorMessage) {
+        auditService.record(context.operationType(),
+                context.resourceType(),
+                context.definition().name(),
+                context.cluster(),
+                errorMessage,
+                result
+        );
+    }
+
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolCapabilityFilter.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolCapabilityFilter.java
new file mode 100644
index 000000000..dd0b45e4b
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolCapabilityFilter.java
@@ -0,0 +1,54 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.filter;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.ops.ai.tool.catalog.CapabilityResolver;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolInvocation;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolDefinition;
+import org.springframework.stereotype.Component;
+
+import java.util.Set;
+
+@Component
+@RequiredArgsConstructor
+public class ToolCapabilityFilter implements ToolExecutionFilter {
+
+    private final CapabilityResolver capabilityResolver;
+
+    @Override
+    public Type type() {
+        return Type.CAPABILITY;
+    }
+
+    @Override
+    public Object filter(ToolInvocation invocation, Chain chain) {
+        ToolExecutionContext context = invocation.context();
+        ToolDefinition definition = context.definition();
+        if (definition.requiredCapabilities().isEmpty()) {
+            return chain.proceed(invocation);
+        }
+
+        Set<String> capabilities = capabilityResolver.resolve(context.cluster());
+        if (!capabilities.containsAll(definition.requiredCapabilities())) {
+            throw ToolError.TOOL_CAPABILITY_UNSUPPORTED.exception(definition.name());
+        }
+        return chain.proceed(invocation);
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolExecutionFilter.java
similarity index 66%
copy from server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolExecutionFilter.java
index 2fcbb55d6..c1da3f5e8 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/common/exception/BusinessException.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolExecutionFilter.java
@@ -14,16 +14,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.common.exception;
+package org.apache.rocketmq.studio.ops.ai.tool.filter;
 
-import lombok.Getter;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolInvocation;
 
-@Getter
-public class BusinessException extends RuntimeException {
-    private final int code;
+public interface ToolExecutionFilter {
 
-    public BusinessException(int code, String message) {
-        super(message);
-        this.code = code;
+    enum Type {
+        AUDIT,
+        VALIDATION,
+        CAPABILITY,
+        MUTATION
+    }
+
+    Type type();
+
+    Object filter(ToolInvocation invocation, Chain chain);
+
+    interface Chain {
+        Object proceed(ToolInvocation invocation);
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolFilterChain.java
similarity index 50%
copy from server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolFilterChain.java
index 44b4a2fbf..93e0589e4 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolFilterChain.java
@@ -14,30 +14,32 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.ops.ai.tool;
+package org.apache.rocketmq.studio.ops.ai.tool.filter;
 
-import org.apache.rocketmq.studio.cluster.nameserver.NameServerConfigDiffService;
-
-import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolInvocation;
 import org.springframework.stereotype.Component;
 
-import java.util.Map;
+import java.util.Comparator;
+import java.util.List;
 
 @Component
-@RequiredArgsConstructor
-public class NameServerConfigDiffToolHandler implements ToolHandler {
-
-    private static final String NAME = "rmq.nameserver.config.diff";
-
-    private final NameServerConfigDiffService configDiffService;
-
-    @Override
-    public String name() {
-        return NAME;
+public final class ToolFilterChain {
+
+    private final ToolExecutionFilter.Chain chain;
+
+    public ToolFilterChain(List<ToolExecutionFilter> filters) {
+        List<ToolExecutionFilter> ordered = filters.stream()
+                .sorted(Comparator.comparing(ToolExecutionFilter::type))
+                .toList();
+        ToolExecutionFilter.Chain action = ToolInvocation::execute;
+        for (ToolExecutionFilter filter : ordered.reversed()) {
+            ToolExecutionFilter.Chain next = action;
+            action = invocation -> filter.filter(invocation, next);
+        }
+        this.chain = action;
     }
 
-    @Override
-    public Object execute(Map<String, Object> input) {
-        return configDiffService.compare((String) input.get("cluster"));
+    public Object execute(ToolInvocation invocation) {
+        return chain.proceed(invocation);
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolMutationFilter.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolMutationFilter.java
new file mode 100644
index 000000000..12013ac1b
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolMutationFilter.java
@@ -0,0 +1,89 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.filter;
+
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.MutationOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolDefinition;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolInvocation;
+import org.apache.rocketmq.studio.ops.ai.tool.service.ToolTokenService;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ToolMutationFilter implements ToolExecutionFilter {
+
+    private static final String L3_WARNING =
+            "This high-risk operation requires break-glass approval and an explicit reason.";
+
+    private final ToolTokenService tokenService;
+    private final boolean l3Enabled;
+
+    public ToolMutationFilter(
+            ToolTokenService tokenService,
+            @Value("${studio.ai.allow-l3-tools:false}") boolean l3Enabled) {
+        this.tokenService = tokenService;
+        this.l3Enabled = l3Enabled;
+    }
+
+    @Override
+    public Type type() {
+        return Type.MUTATION;
+    }
+
+    @Override
+    public Object filter(ToolInvocation invocation, Chain chain) {
+        ToolExecutionContext context = invocation.context();
+        if (context.definition().isReadOnly()) {
+            return chain.proceed(invocation);
+        }
+        if (!context.dryRun()) {
+            if (context.definition().requiresReason()) {
+                verifyL3Requirements(context);
+            }
+            tokenService.verify(context);
+        }
+
+        ToolPlan plan = invocation.preview();
+        if (context.definition().requiresReason()) {
+            plan = plan.withWarning(L3_WARNING);
+        }
+        if (context.dryRun()) {
+            String token = tokenService.issue(context);
+            return new MutationOutput<>(MutationOutput.Status.PLANNED, context.cluster(), plan, token, null);
+        }
+
+        Object result = chain.proceed(invocation);
+        return new MutationOutput<>(MutationOutput.Status.EXECUTED, context.cluster(), plan, null, result);
+    }
+
+    private void verifyL3Requirements(ToolExecutionContext context) {
+        ToolDefinition definition = context.definition();
+        if (!l3Enabled) {
+            throw ToolError.L3_DISABLED.exception();
+        }
+        if (!context.breakGlass()) {
+            throw ToolError.L3_APPROVAL_REQUIRED.exception(definition.name());
+        }
+        String reason = context.reason();
+        if (reason == null || reason.isBlank()) {
+            throw ToolError.L3_REASON_REQUIRED.exception(definition.name());
+        }
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolValidationFilter.java
similarity index 54%
copy from server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolValidationFilter.java
index 44b4a2fbf..b6d2acb54 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolValidationFilter.java
@@ -14,30 +14,31 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.cluster.nameserver.NameServerConfigDiffService;
+package org.apache.rocketmq.studio.ops.ai.tool.filter;
 
 import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolInvocation;
+import org.apache.rocketmq.studio.ops.ai.tool.service.ToolSchemaValidator;
 import org.springframework.stereotype.Component;
 
-import java.util.Map;
-
 @Component
 @RequiredArgsConstructor
-public class NameServerConfigDiffToolHandler implements ToolHandler {
-
-    private static final String NAME = "rmq.nameserver.config.diff";
+public class ToolValidationFilter implements ToolExecutionFilter {
 
-    private final NameServerConfigDiffService configDiffService;
+    private final ToolSchemaValidator schemaValidator;
 
     @Override
-    public String name() {
-        return NAME;
+    public Type type() {
+        return Type.VALIDATION;
     }
 
     @Override
-    public Object execute(Map<String, Object> input) {
-        return configDiffService.compare((String) input.get("cluster"));
+    public Object filter(ToolInvocation invocation, Chain chain) {
+        ToolExecutionContext context = invocation.context();
+        schemaValidator.validateInput(context.definition(), context.input());
+        Object result = chain.proceed(invocation);
+        schemaValidator.validateOutput(context.definition(), result);
+        return result;
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/CapabilitiesToolHandler.java
similarity index 51%
copy from server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/CapabilitiesToolHandler.java
index 44b4a2fbf..176a1a8a7 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/CapabilitiesToolHandler.java
@@ -14,30 +14,36 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.cluster.nameserver.NameServerConfigDiffService;
+package org.apache.rocketmq.studio.ops.ai.tool.handler;
 
+import org.apache.rocketmq.studio.ops.ai.tool.catalog.CapabilityResolver;
 import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.CapabilitiesOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterInput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
 import org.springframework.stereotype.Component;
 
-import java.util.Map;
-
 @Component
 @RequiredArgsConstructor
-public class NameServerConfigDiffToolHandler implements ToolHandler {
+public class CapabilitiesToolHandler implements ToolHandler<ClusterInput, CapabilitiesOutput> {
 
-    private static final String NAME = "rmq.nameserver.config.diff";
-
-    private final NameServerConfigDiffService configDiffService;
+    private final CapabilityResolver capabilityResolver;
 
     @Override
     public String name() {
-        return NAME;
+        return "rmq.capabilities";
     }
 
     @Override
-    public Object execute(Map<String, Object> input) {
-        return configDiffService.compare((String) input.get("cluster"));
+    public Class<ClusterInput> inputType() {
+        return ClusterInput.class;
     }
+
+    @Override
+    public CapabilitiesOutput execute(ClusterInput input, ToolExecutionContext context) {
+        return new CapabilitiesOutput(context.cluster(),
+                capabilityResolver.resolve(context.cluster()).stream().sorted().toList());
+    }
+
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/MutationToolHandler.java
similarity index 56%
copy from server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/MutationToolHandler.java
index 141b1cef2..16ae3bcac 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/cluster/broker/ClusterProvider.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/MutationToolHandler.java
@@ -14,26 +14,25 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.cluster.broker;
+package org.apache.rocketmq.studio.ops.ai.tool.handler;
 
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
 
-import java.util.List;
+public abstract class MutationToolHandler<I, R>
+        implements ToolHandler<I, R> {
 
-public interface ClusterProvider {
+    private final Class<I> inputType;
 
-    List<ClusterVO> discoverClusters();
-
-    default List<ClusterVO> discoverClusters(String instanceId) {
-        return discoverClusters();
+    protected MutationToolHandler(Class<I> inputType) {
+        this.inputType = inputType;
     }
 
-    default List<ClusterVO> discoverClustersAt(String namesrvAddr) {
-        return List.of();
+    @Override
+    public final Class<I> inputType() {
+        return inputType;
     }
 
-    ClusterVO refreshClusterDetail(String clusterId);
-
-    default ClusterVO refreshClusterDetail(String clusterId, String instanceId) {
-        return refreshClusterDetail(clusterId);
-    }
+    public abstract ToolPlan preview(I input, ToolExecutionContext context);
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclCreateToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclCreateToolHandler.java
new file mode 100644
index 000000000..dfcb924df
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclCreateToolHandler.java
@@ -0,0 +1,62 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.handler.acl;
+
+import org.apache.rocketmq.studio.instance.acl.AclRuleVO;
+import org.apache.rocketmq.studio.instance.acl.AclService;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclMutationInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.PlanDescription;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class AclCreateToolHandler extends MutationToolHandler<AclMutationInput, AclRuleVO> {
+
+    private static final PlanDescription PLAN_DESCRIPTION = new PlanDescription(
+            "create ACL rule '%s:%s' in cluster '%s'.",
+            List.of("Adds a rule to the ACL policy evaluated for broker requests."),
+            List.of("The new ACL rule changes permissions for subsequent broker requests."));
+
+    private final AclService aclService;
+
+    public AclCreateToolHandler(AclService aclService) {
+        super(AclMutationInput.class);
+        this.aclService = aclService;
+    }
+
+    @Override
+    public String name() {
+        return "rmq.acl.create";
+    }
+
+    @Override
+    public ToolPlan preview(AclMutationInput input, ToolExecutionContext context) {
+        return PLAN_DESCRIPTION.builder(input.principal(), input.resource(), context.cluster())
+                .after(AclMutationInput.from(input.toRule(null)))
+                .build();
+    }
+
+    @Override
+    public AclRuleVO execute(AclMutationInput input, ToolExecutionContext context) {
+        return aclService.createRule(input.toRule(null), context.cluster());
+    }
+
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclDeleteToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclDeleteToolHandler.java
new file mode 100644
index 000000000..59fe4367f
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclDeleteToolHandler.java
@@ -0,0 +1,64 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.handler.acl;
+
+import org.apache.rocketmq.studio.instance.acl.AclService;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ResourceDeleteInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclMutationInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.PlanDescription;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class AclDeleteToolHandler extends MutationToolHandler<ResourceDeleteInput, Void> {
+
+    private static final PlanDescription PLAN_DESCRIPTION = new PlanDescription(
+            "delete ACL rule '%s' in cluster '%s'.",
+            List.of("Permanently removes the selected ACL rule."),
+            List.of("Removing this rule changes permissions for subsequent broker requests."));
+
+    private final AclService aclService;
+
+    public AclDeleteToolHandler(AclService aclService) {
+        super(ResourceDeleteInput.class);
+        this.aclService = aclService;
+    }
+
+    @Override
+    public String name() {
+        return "rmq.acl.delete";
+    }
+
+    @Override
+    public ToolPlan preview(ResourceDeleteInput input, ToolExecutionContext context) {
+        AclMutationInput before = AclMutationInput.from(
+                aclService.getRule(input.id(), context.cluster()));
+        return PLAN_DESCRIPTION.builder(input.id(), context.cluster())
+                .before(before)
+                .build();
+    }
+
+    @Override
+    public Void execute(ResourceDeleteInput input, ToolExecutionContext context) {
+        aclService.deleteRule(input.id(), context.cluster());
+        return null;
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclListToolHandler.java
new file mode 100644
index 000000000..5ec41fa3f
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclListToolHandler.java
@@ -0,0 +1,54 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.handler.acl;
+
+import org.apache.rocketmq.studio.common.domain.PageResult;
+import org.apache.rocketmq.studio.instance.acl.AclRuleVO;
+import org.apache.rocketmq.studio.instance.acl.AclService;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclRuleItem;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.PageOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclListInput;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class AclListToolHandler implements ToolHandler<AclListInput, PageOutput<AclRuleItem>> {
+
+    private final AclService aclService;
+
+    @Override
+    public String name() {
+        return "rmq.acl.list";
+    }
+
+    @Override
+    public Class<AclListInput> inputType() {
+        return AclListInput.class;
+    }
+
+    @Override
+    public PageOutput<AclRuleItem> execute(AclListInput input, ToolExecutionContext context) {
+        PageResult<AclRuleVO> result = aclService.listRules(
+                input.principal(), input.resource(), input.scope(),
+                input.decision(), input.aclVersion(), context.cluster(),
+                input.page().page(), input.page().pageSize());
+        return PageOutput.from(result, AclRuleItem::from);
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclUpdateToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclUpdateToolHandler.java
new file mode 100644
index 000000000..0f278fed7
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/AclUpdateToolHandler.java
@@ -0,0 +1,79 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.handler.acl;
+
+import org.apache.rocketmq.studio.instance.acl.AclRuleVO;
+import org.apache.rocketmq.studio.instance.acl.AclService;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclMutationInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.PlanDescription;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class AclUpdateToolHandler extends MutationToolHandler<AclMutationInput, AclRuleVO> {
+
+    private static final PlanDescription PLAN_DESCRIPTION = new PlanDescription(
+            "update ACL rule '%s' in cluster '%s'.",
+            List.of("Replaces the selected ACL rule."),
+            List.of("The updated ACL rule changes permissions for subsequent broker requests."));
+
+    private final AclService aclService;
+
+    public AclUpdateToolHandler(AclService aclService) {
+        super(AclMutationInput.class);
+        this.aclService = aclService;
+    }
+
+    @Override
+    public String name() {
+        return "rmq.acl.update";
+    }
+
+    @Override
+    public ToolPlan preview(AclMutationInput input, ToolExecutionContext context) {
+        String instanceId = context.cluster();
+        AclMutationInput before = AclMutationInput.from(aclService.getRule(input.id(), instanceId));
+        AclMutationInput after = AclMutationInput.from(input.toRule(parseId(input.id())));
+        return PLAN_DESCRIPTION.builder(input.id(), context.cluster())
+                .before(before)
+                .after(after)
+                .warningIf(before.equals(after),
+                        "The requested ACL rule already matches the current state.")
+                .build();
+    }
+
+    @Override
+    public AclRuleVO execute(AclMutationInput input, ToolExecutionContext context) {
+        return aclService.updateRule(input.toRule(parseId(input.id())), context.cluster());
+    }
+
+    private static Long parseId(String id) {
+        if (id == null || id.isBlank()) {
+            throw ToolError.ACL_ID_INVALID.exception(id);
+        }
+        try {
+            return Long.parseLong(id);
+        } catch (NumberFormatException e) {
+            throw ToolError.ACL_ID_INVALID.exception(id);
+        }
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserCreateToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserCreateToolHandler.java
new file mode 100644
index 000000000..b8d18ddde
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserCreateToolHandler.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.rocketmq.studio.ops.ai.tool.handler.acl;
+
+import org.apache.rocketmq.studio.instance.acl.AclService;
+import org.apache.rocketmq.studio.instance.acl.AclUserVO;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclUserItem;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.UserCreateInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.PlanDescription;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class UserCreateToolHandler extends MutationToolHandler<UserCreateInput, AclUserItem> {
+
+    private static final PlanDescription PLAN_DESCRIPTION = new PlanDescription(
+            "create user '%s' in cluster '%s'.",
+            List.of("Creates an ACL identity; credentials are generated only during apply."),
+            List.of());
+
+    private final AclService aclService;
+
+    public UserCreateToolHandler(AclService aclService) {
+        super(UserCreateInput.class);
+        this.aclService = aclService;
+    }
+
+    @Override
+    public String name() {
+        return "rmq.user.create";
+    }
+
+    @Override
+    public ToolPlan preview(UserCreateInput input, ToolExecutionContext context) {
+        String instanceId = context.cluster();
+        AclUserItem before = aclService.listUsers(instanceId).stream()
+                .filter(user -> input.username().equals(user.getUsername()))
+                .findFirst().map(AclUserItem::from).orElse(null);
+        AclUserItem after = new AclUserItem(
+                null, input.username(), Boolean.TRUE.equals(input.admin()), input.clusters());
+        return PLAN_DESCRIPTION.builder(input.username(), context.cluster())
+                .before(before)
+                .after(after)
+                .build();
+    }
+
+    @Override
+    public AclUserItem execute(UserCreateInput input, ToolExecutionContext context) {
+        AclUserVO user = AclUserVO.builder()
+                .username(input.username())
+                .admin(Boolean.TRUE.equals(input.admin()))
+                .clusters(input.clusters())
+                .build();
+        return AclUserItem.from(aclService.createUser(user, context.cluster()));
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserDeleteToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserDeleteToolHandler.java
new file mode 100644
index 000000000..91f78db41
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserDeleteToolHandler.java
@@ -0,0 +1,64 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.handler.acl;
+
+import org.apache.rocketmq.studio.instance.acl.AclService;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclUserItem;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ResourceDeleteInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.PlanDescription;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class UserDeleteToolHandler extends MutationToolHandler<ResourceDeleteInput, Void> {
+
+    private static final PlanDescription PLAN_DESCRIPTION = new PlanDescription(
+            "delete user '%s' in cluster '%s'.",
+            List.of("Deletes the ACL identity and its generated credentials."),
+            List.of());
+
+    private final AclService aclService;
+
+    public UserDeleteToolHandler(AclService aclService) {
+        super(ResourceDeleteInput.class);
+        this.aclService = aclService;
+    }
+
+    @Override
+    public String name() {
+        return "rmq.user.delete";
+    }
+
+    @Override
+    public ToolPlan preview(ResourceDeleteInput input, ToolExecutionContext context) {
+        AclUserItem before = AclUserItem.from(aclService.getUser(
+                input.id(), context.cluster()));
+        return PLAN_DESCRIPTION.builder(input.id(), context.cluster())
+                .before(before)
+                .build();
+    }
+
+    @Override
+    public Void execute(ResourceDeleteInput input, ToolExecutionContext context) {
+        aclService.deleteUser(input.id(), context.cluster());
+        return null;
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserListToolHandler.java
similarity index 50%
copy from server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java
copy to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserListToolHandler.java
index 44b4a2fbf..81d53ee2f 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/NameServerConfigDiffToolHandler.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/acl/UserListToolHandler.java
@@ -14,30 +14,36 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.ops.ai.tool;
-
-import org.apache.rocketmq.studio.cluster.nameserver.NameServerConfigDiffService;
+package org.apache.rocketmq.studio.ops.ai.tool.handler.acl;
 
 import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.instance.acl.AclService;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclUserItem;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ListOutput;
 import org.springframework.stereotype.Component;
 
-import java.util.Map;
-
 @Component
 @RequiredArgsConstructor
-public class NameServerConfigDiffToolHandler implements ToolHandler {
-
-    private static final String NAME = "rmq.nameserver.config.diff";
-
-    private final NameServerConfigDiffService configDiffService;
+public class UserListToolHandler implements ToolHandler<ClusterInput, ListOutput<AclUserItem>> {
+    private final AclService aclService;
 
     @Override
     public String name() {
-        return NAME;
+        return "rmq.user.list";
+    }
+
+    @Override
+    public Class<ClusterInput> inputType() {
+        return ClusterInput.class;
     }
 
     @Override
-    public Object execute(Map<String, Object> input) {
-        return configDiffService.compare((String) input.get("cluster"));
+    public ListOutput<AclUserItem> execute(
+            ClusterInput input, ToolExecutionContext context) {
+        return new ListOutput<>(aclService.listUsers(context.cluster()).stream()
+                .map(AclUserItem::from).toList());
     }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/alert/AlertRuleListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/alert/AlertRuleListToolHandler.java
new file mode 100644
index 000000000..c7eccf10c
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/alert/AlertRuleListToolHandler.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.rocketmq.studio.ops.ai.tool.handler.alert;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.alert.AlertRuleListInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.alert.AlertRuleListItem;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ListOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import org.apache.rocketmq.studio.ops.alert.AlertRuleVO;
+import org.apache.rocketmq.studio.ops.alert.AlertService;
+import org.springframework.stereotype.Component;
+
+import java.util.Locale;
+
+@Component
+@RequiredArgsConstructor
+public class AlertRuleListToolHandler
+        implements ToolHandler<AlertRuleListInput, ListOutput<AlertRuleListItem>> {
+
+    private final AlertService alertService;
+
+    @Override
+    public String name() {
+        return "rmq.alert.rule.list";
+    }
+
+    @Override
+    public Class<AlertRuleListInput> inputType() {
+        return AlertRuleListInput.class;
+    }
+
+    @Override
+    public ListOutput<AlertRuleListItem> execute(
+            AlertRuleListInput input, ToolExecutionContext context) {
+        return new ListOutput<>(alertService.findRules(context.cluster()).stream()
+                .filter(rule -> matchesEnabled(rule, input.enabled()))
+                .filter(rule -> matchesSearch(rule, input.search()))
+                .map(AlertRuleListItem::from)
+                .toList());
+    }
+
+    private static boolean matchesEnabled(AlertRuleVO rule, Boolean enabled) {
+        return enabled == null || rule.isEnabled() == enabled;
+    }
+
+    private static boolean matchesSearch(AlertRuleVO rule, String search) {
+        if (search == null || search.isBlank()) {
+            return true;
+        }
+        String normalizedSearch = search.trim().toLowerCase(Locale.ROOT);
+        return contains(rule.getName(), normalizedSearch)
+                || contains(rule.getMetric(), normalizedSearch)
+                || contains(rule.getDescription(), normalizedSearch);
+    }
+
+    private static boolean contains(String value, String normalizedSearch) {
+        return value != null && value.toLowerCase(Locale.ROOT).contains(normalizedSearch);
+    }
+
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerConfigToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerConfigToolHandler.java
new file mode 100644
index 000000000..1441f1005
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerConfigToolHandler.java
@@ -0,0 +1,50 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.handler.broker;
+
+import org.apache.rocketmq.studio.cluster.broker.BrokerConfigDiffService;
+import org.apache.rocketmq.studio.cluster.config.BrokerConfigDiffVO;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.broker.BrokerConfigOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.BrokerClusterInput;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class BrokerConfigToolHandler implements ToolHandler<BrokerClusterInput, BrokerConfigOutput> {
+
+    private final BrokerConfigDiffService brokerConfigDiffService;
+
+    @Override
+    public String name() {
+        return "rmq.broker.config";
+    }
+
+    @Override
+    public Class<BrokerClusterInput> inputType() {
+        return BrokerClusterInput.class;
+    }
+
+    @Override
+    public BrokerConfigOutput execute(
+            BrokerClusterInput input, ToolExecutionContext context) {
+        BrokerConfigDiffVO diff = brokerConfigDiffService.compareForInstance(context.cluster());
+        return BrokerConfigOutput.from(diff, context.cluster());
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerConfigUpdateToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerConfigUpdateToolHandler.java
new file mode 100644
index 000000000..be88e0fd6
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerConfigUpdateToolHandler.java
@@ -0,0 +1,84 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.handler.broker;
+
+import org.apache.rocketmq.studio.cluster.broker.ClusterService;
+import org.apache.rocketmq.studio.cluster.config.ClusterConfigUpdateResultVO;
+import org.apache.rocketmq.studio.cluster.config.UpdateConfigDTO;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.broker.BrokerConfigUpdateInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.broker.BrokerConfigUpdateOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.PlanDescription;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class BrokerConfigUpdateToolHandler extends MutationToolHandler<BrokerConfigUpdateInput, BrokerConfigUpdateOutput> {
+
+    private static final PlanDescription PLAN_DESCRIPTION = new PlanDescription(
+            "update Broker configuration in Instance '%s'.",
+            List.of(),
+            List.of());
+
+    private final ClusterService clusterService;
+
+    public BrokerConfigUpdateToolHandler(ClusterService clusterService) {
+        super(BrokerConfigUpdateInput.class);
+        this.clusterService = clusterService;
+    }
+
+    @Override
+    public String name() {
+        return "rmq.broker.config_update";
+    }
+
+    @Override
+    public ToolPlan preview(BrokerConfigUpdateInput input, ToolExecutionContext context) {
+        var current = clusterService.readBrokerConfigs(context.cluster());
+        var proposed = clusterService.proposeBrokerConfigs(toCommand(input, context.cluster()), current);
+        return PLAN_DESCRIPTION.builder(context.cluster())
+                .before(java.util.Map.of("brokers", current))
+                .after(java.util.Map.of("brokers", proposed))
+                .impact("Updates the requested configuration fields on " + current.size() + " master Broker(s).")
+                .build();
+    }
+
+    @Override
+    public BrokerConfigUpdateOutput execute(
+            BrokerConfigUpdateInput input, ToolExecutionContext context) {
+        ClusterConfigUpdateResultVO result = clusterService.updateClusterConfig(
+                toCommand(input, context.cluster()), context.cluster());
+        return BrokerConfigUpdateOutput.from(result);
+    }
+
+    private static UpdateConfigDTO toCommand(BrokerConfigUpdateInput input, String instanceId) {
+        return UpdateConfigDTO.builder()
+                .instanceId(instanceId)
+                .flushDiskType(input.flushDiskType())
+                .autoCreateTopicEnable(input.autoCreateTopicEnable())
+                .autoCreateSubscriptionGroup(input.autoCreateSubscriptionGroup())
+                .maxMessageSize(input.maxMessageSize())
+                .fileReservedTime(input.fileReservedTime())
+                .writeQueueNums(input.writeQueueNums())
+                .readQueueNums(input.readQueueNums())
+                .brokerPermission(input.brokerPermission())
+                .build();
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerDescribeToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerDescribeToolHandler.java
new file mode 100644
index 000000000..cbb49ecba
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerDescribeToolHandler.java
@@ -0,0 +1,53 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.handler.broker;
+
+import org.apache.rocketmq.studio.cluster.broker.BrokerVO;
+import org.apache.rocketmq.studio.cluster.broker.ClusterProvider;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.broker.BrokerDescribeInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterListOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class BrokerDescribeToolHandler
+        implements ToolHandler<BrokerDescribeInput, ClusterListOutput<BrokerVO>> {
+
+    private final ClusterProvider clusterProvider;
+
+    @Override
+    public String name() {
+        return "rmq.broker.describe";
+    }
+
+    @Override
+    public Class<BrokerDescribeInput> inputType() {
+        return BrokerDescribeInput.class;
+    }
+
+    @Override
+    public ClusterListOutput<BrokerVO> execute(
+            BrokerDescribeInput input, ToolExecutionContext context) {
+        List<BrokerVO> matched = clusterProvider.discoverBrokers(context.cluster(), input.broker());
+        return new ClusterListOutput<>(context.cluster(), matched);
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerListToolHandler.java
new file mode 100644
index 000000000..c465553b8
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerListToolHandler.java
@@ -0,0 +1,53 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.handler.broker;
+
+import org.apache.rocketmq.studio.cluster.broker.ClusterProvider;
+import org.apache.rocketmq.studio.cluster.broker.BrokerVO;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterListOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class BrokerListToolHandler
+        implements ToolHandler<ClusterInput, ClusterListOutput<BrokerVO>> {
+
+    private final ClusterProvider clusterProvider;
+
+    @Override
+    public String name() {
+        return "rmq.broker.list";
+    }
+
+    @Override
+    public Class<ClusterInput> inputType() {
+        return ClusterInput.class;
+    }
+
+    @Override
+    public ClusterListOutput<BrokerVO> execute(
+            ClusterInput input, ToolExecutionContext context) {
+        List<BrokerVO> brokers = clusterProvider.discoverBrokers(context.cluster(), null);
+        return new ClusterListOutput<>(context.cluster(), brokers);
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/McpServerImpl.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerRuntimeInfoToolHandler.java
similarity index 50%
rename from server/src/main/java/org/apache/rocketmq/studio/ops/ai/McpServerImpl.java
rename to server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerRuntimeInfoToolHandler.java
index ebaf55f0e..afb1cd63d 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/McpServerImpl.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/broker/BrokerRuntimeInfoToolHandler.java
@@ -14,50 +14,40 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.rocketmq.studio.ops.ai;
+package org.apache.rocketmq.studio.ops.ai.tool.handler.broker;
 
-import org.apache.rocketmq.studio.ops.ai.tool.ToolCatalog;
-import org.apache.rocketmq.studio.ops.ai.tool.ToolGatewayService;
+import org.apache.rocketmq.studio.cluster.broker.ClusterProvider;
+import org.apache.rocketmq.studio.cluster.broker.BrokerVO;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterInput;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
 
 import java.util.List;
-import java.util.Map;
 
 @Component
 @RequiredArgsConstructor
-public class McpServerImpl implements McpServerRegistry {
+public class BrokerRuntimeInfoToolHandler
+        implements ToolHandler<ClusterInput, BrokerRuntimeInfoToolHandler.Output> {
 
-    private final ToolGatewayService toolGatewayService;
-    private final ToolCatalog toolCatalog;
+    private final ClusterProvider clusterProvider;
 
     @Override
-    public List<AiToolVO> listTools() {
-        return toolGatewayService.discover(null);
+    public String name() {
+        return "rmq.broker.runtime_info";
     }
 
     @Override
-    public List<AiToolVO> listTools(String clusterId) {
-        return toolGatewayService.discover(clusterId);
+    public Class<ClusterInput> inputType() {
+        return ClusterInput.class;
     }
 
     @Override
-    public Object execute(String name, Map<String, Object> input) {
-        return toolGatewayService.execute(name, input);
+    public Output execute(
+            ClusterInput input, ToolExecutionContext context) {
+        return new Output(context.cluster(), clusterProvider.discoverBrokers(context.cluster(), null));
     }
 
-    @Override
-    public String catalogVersion() {
-        return toolCatalog.getVersion();
-    }
-
-    @Override
-    public String catalogDigest() {
-        return toolCatalog.getDigest();
-    }
-
-    @Override
-    public String minimumClientVersion() {
-        return toolCatalog.getMinimumClientVersion();
-    }
+    public record Output(String cluster, List<BrokerVO> brokers) { }
 }
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/client/ClientDescribeToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/client/ClientDescribeToolHandler.java
new file mode 100644
index 000000000..790df4ed0
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/client/ClientDescribeToolHandler.java
@@ -0,0 +1,64 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.handler.client;
+
+import org.apache.rocketmq.studio.cluster.client.ClientConnectionVO;
+import org.apache.rocketmq.studio.cluster.client.ClientService;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.client.ClientDescribeInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterListOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class ClientDescribeToolHandler
+        implements ToolHandler<ClientDescribeInput, ClusterListOutput<ClientConnectionVO>> {
+
+    private final ClientService clientService;
+
+    @Override
+    public String name() {
+        return "rmq.client.describe";
+    }
+
+    @Override
+    public Class<ClientDescribeInput> inputType() {
+        return ClientDescribeInput.class;
+    }
+
+    @Override
+    public ClusterListOutput<ClientConnectionVO> execute(
+            ClientDescribeInput input, ToolExecutionContext context) {
+        String instanceId = context.cluster();
+        String clusterFilter = null;
+        return describe(input, instanceId, clusterFilter, context.cluster());
+    }
+
+    private ClusterListOutput<ClientConnectionVO> describe(
+            ClientDescribeInput input, String instanceId, String clusterFilter, String cluster) {
+        List<ClientConnectionVO> connections = clientService.listConnections(
+                instanceId, clusterFilter, input.type());
+        List<ClientConnectionVO> matched = connections.stream()
+                .filter(c -> input.clientId().equals(c.getClientId()))
+                .toList();
+        return new ClusterListOutput<>(cluster, matched);
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/client/ClientListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/client/ClientListToolHandler.java
new file mode 100644
index 000000000..066e7b2a6
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/client/ClientListToolHandler.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.rocketmq.studio.ops.ai.tool.handler.client;
+
+import org.apache.rocketmq.studio.cluster.client.ClientConnectionVO;
+import org.apache.rocketmq.studio.cluster.client.ClientService;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.client.ClientListInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterListOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class ClientListToolHandler
+        implements ToolHandler<ClientListInput, ClusterListOutput<ClientConnectionVO>> {
+
+    private final ClientService clientService;
+
+    @Override
+    public String name() {
+        return "rmq.client.list";
+    }
+
+    @Override
+    public Class<ClientListInput> inputType() {
+        return ClientListInput.class;
+    }
+
+    @Override
+    public ClusterListOutput<ClientConnectionVO> execute(
+            ClientListInput input, ToolExecutionContext context) {
+        String instanceId = context.cluster();
+        String clusterFilter = null;
+        return list(input, instanceId, clusterFilter, context.cluster());
+    }
+
+    private ClusterListOutput<ClientConnectionVO> list(
+            ClientListInput input, String instanceId, String clusterFilter, String cluster) {
+        List<ClientConnectionVO> connections = clientService.listConnections(
+                instanceId, clusterFilter, input.type());
+        return new ClusterListOutput<>(cluster, connections);
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/cluster/ClusterListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/cluster/ClusterListToolHandler.java
new file mode 100644
index 000000000..f376cea7e
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/cluster/ClusterListToolHandler.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.rocketmq.studio.ops.ai.tool.handler.cluster;
+
+import org.apache.rocketmq.studio.ops.ai.tool.contract.cluster.ClusterListItem;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.cluster.ClusterListInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ListOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+
+import org.apache.rocketmq.studio.cluster.broker.ClusterService;
+import org.apache.rocketmq.studio.cluster.broker.ClusterVO;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class ClusterListToolHandler
+        implements ToolHandler<ClusterListInput, ListOutput<ClusterListItem>> {
+
+    private final ClusterService clusterService;
+
+    @Override
+    public String name() {
+        return "rmq.cluster.list";
+    }
+
+    @Override
+    public Class<ClusterListInput> inputType() {
+        return ClusterListInput.class;
+    }
+
+    @Override
+    public ListOutput<ClusterListItem> execute(
+            ClusterListInput input, ToolExecutionContext context) {
+        return new ListOutput<>(clusterService.listClusters(context.cluster()).stream()
+                .filter(cluster -> matchesStatus(cluster, input.status()))
+                .map(ClusterListItem::from)
+                .toList());
+    }
+
+    private static boolean matchesStatus(ClusterVO cluster, String status) {
+        return status == null
+                || cluster.getStatus() != null
+                && cluster.getStatus().name().equalsIgnoreCase(status.trim());
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dashboard/DashboardSummaryToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dashboard/DashboardSummaryToolHandler.java
new file mode 100644
index 000000000..c4b986647
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dashboard/DashboardSummaryToolHandler.java
@@ -0,0 +1,125 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.handler.dashboard;
+
+import org.apache.rocketmq.studio.ops.dashboard.ClusterOverviewVO;
+import org.apache.rocketmq.studio.ops.dashboard.DashboardDataVO;
+import org.apache.rocketmq.studio.ops.dashboard.DashboardService;
+import org.apache.rocketmq.studio.ops.dashboard.DashboardStatsVO;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterInput;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class DashboardSummaryToolHandler
+        implements ToolHandler<ClusterInput, DashboardSummaryToolHandler.Output> {
+
+    private final DashboardService dashboardService;
+
+    @Override
+    public String name() {
+        return "rmq.dashboard.summary";
+    }
+
+    @Override
+    public Class<ClusterInput> inputType() {
+        return ClusterInput.class;
+    }
+
+    @Override
+    public Output execute(ClusterInput input, ToolExecutionContext context) {
+        DashboardDataVO dashboard = dashboardService.getDashboard(context.cluster());
+        return new Output(context.cluster(), clusters(dashboard).stream().map(DashboardSummaryToolHandler::clusterProjection).toList(),
+                statsProjection(dashboard == null ? null : dashboard.getStats()));
+    }
+
+    private static List<ClusterOverviewVO> clusters(DashboardDataVO dashboard) {
+        if (dashboard == null || dashboard.getClusters() == null) {
+            return List.of();
+        }
+        return dashboard.getClusters();
+    }
+
+    private static Cluster clusterProjection(ClusterOverviewVO cluster) {
+        return new Cluster(
+                cluster.getId(),
+                cluster.getName(),
+                cluster.getType() == null ? null : cluster.getType().name(),
+                cluster.getStatus() == null ? null : cluster.getStatus().name(),
+                cluster.getBrokers(),
+                cluster.getProxies(),
+                cluster.getTopics(),
+                cluster.getGroups(),
+                cluster.getTpsIn(),
+                cluster.getTpsOut(),
+                cluster.getVersion(),
+                cluster.getThroughput() == null ? List.of() : cluster.getThroughput());
+    }
+
+    private static Stats statsProjection(DashboardStatsVO stats) {
+        DashboardStatsVO safeStats = stats == null ? new DashboardStatsVO() : stats;
+        return new Stats(
+                safeStats.getTotalClusters(),
+                safeStats.getHealthyClusters(),
+                safeStats.getTotalBrokers(),
+                safeStats.getTotalProxies(),
+                safeStats.getTotalNameServers(),
+                safeStats.getTotalTopics(),
+                safeStats.getTotalConsumerGroups(),
+                safeStats.getTotalMessagesToday(),
+                safeStats.getMessagesPerSecond(),
+                safeStats.getTpsIn(),
+                safeStats.getTpsOut());
+    }
+
+    public record Output(String cluster, List<Cluster> clusters, Stats stats) {
+    }
+
+    public record Cluster(
+            String id,
+            String name,
+            String type,
+            String status,
+            int brokers,
+            Integer proxies,
+            int topics,
+            int groups,
+            long tpsIn,
+            long tpsOut,
+            String version,
+            List<Integer> throughput) {
+    }
+
+    public record Stats(
+            int totalClusters,
+            int healthyClusters,
+            int totalBrokers,
+            Integer totalProxies,
+            Integer totalNameServers,
+            int totalTopics,
+            int totalConsumerGroups,
+            long totalMessagesToday,
+            long messagesPerSecond,
+            long tpsIn,
+            long tpsOut) {
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dlq/DLQClearToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dlq/DLQClearToolHandler.java
new file mode 100644
index 000000000..0dec3362a
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dlq/DLQClearToolHandler.java
@@ -0,0 +1,84 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.handler.dlq;
+
+import org.apache.rocketmq.studio.instance.topic.MetadataService;
+import org.apache.rocketmq.studio.instance.dlq.DLQGroupVO;
+import org.apache.rocketmq.studio.instance.dlq.DLQService;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolError;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.handler.MutationToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.dlq.DLQClearInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.PlanDescription;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@Component
+public class DLQClearToolHandler extends MutationToolHandler<DLQClearInput, Void> {
+
+    private static final PlanDescription PLAN_DESCRIPTION = new PlanDescription(
+            "clear DLQ for consumer group '%s' in cluster '%s'.",
+            List.of("Deletes the physical dead-letter topic and its retained messages."),
+            List.of("All retained dead-letter messages for this group will be deleted."));
+
+    private final MetadataService metadataService;
+    private final DLQService dlqService;
+
+    public DLQClearToolHandler(MetadataService metadataService, DLQService dlqService) {
+        super(DLQClearInput.class);
+        this.metadataService = metadataService;
+        this.dlqService = dlqService;
+    }
+
+    @Override
+    public String name() {
+        return "rmq.dlq.clear";
+    }
+
+    @Override
+    public ToolPlan preview(DLQClearInput input, ToolExecutionContext context) {
+        String instanceId = context.cluster();
+        String groupName = input.group();
+        DLQGroupVO group = dlqService.listDLQGroups(instanceId, groupName, 1, 100)
+                .getItems().stream()
+                .filter(item -> groupName.equals(item.getGroupName()))
+                .findFirst()
+                .orElseThrow(() -> ToolError.DLQ_GROUP_NOT_FOUND.exception(groupName));
+        if (!group.isStatsAvailable()) {
+            throw ToolError.DLQ_STATISTICS_UNAVAILABLE.exception(groupName);
+        }
+        Map<String, Object> before = new LinkedHashMap<>();
+        before.put("group", group.getGroupName());
+        before.put("topic", group.getDlqTopic());
+        before.put("messageCount", group.getMessageCount());
+        before.put("statsAvailable", group.isStatsAvailable());
+        before.put("status", group.getStatus());
+        return PLAN_DESCRIPTION.builder(groupName, context.cluster())
+                .before(before)
+                .build();
+    }
+
+    @Override
+    public Void execute(DLQClearInput input, ToolExecutionContext context) {
+        metadataService.deleteTopic(context.cluster(), "%DLQ%" + input.group());
+        return null;
+    }
+}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dlq/DLQListToolHandler.java b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dlq/DLQListToolHandler.java
new file mode 100644
index 000000000..e557d7de6
--- /dev/null
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/dlq/DLQListToolHandler.java
@@ -0,0 +1,87 @@
+/*
+ * 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.rocketmq.studio.ops.ai.tool.handler.dlq;
+
+import org.apache.rocketmq.studio.common.domain.PageResult;
+import org.apache.rocketmq.studio.instance.dlq.DLQGroupVO;
+import org.apache.rocketmq.studio.instance.dlq.DLQMessageVO;
+import org.apache.rocketmq.studio.instance.dlq.DLQService;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionContext;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.dlq.DLQListInput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.dlq.DLQListOutput;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class DLQListToolHandler implements ToolHandler<DLQListInput, DLQListOutput> {
+
+    private final DLQService dlqService;
+
+    @Override
+    public String name() {
+        return "rmq.dlq.list";
+    }
+
+    @Override
+    public Class<DLQListInput> inputType() {
+        return DLQListInput.class;
+    }
+
+    @Override
+    public DLQListOutput execute(DLQListInput input, ToolExecutionContext context) {
+        int page = input.page() != null ? input.page().page() : 1;
+        int pageSize = input.page() != null ? input.page().pageSize() : 20;
+        Long startTime = input.time() != null ? input.time().startTime() : null;
+        Long endTime = input.time() != null ? input.time().endTime() : null;
+        if (input.group() == null || input.group().isBlank()) {
+            return listGroups(
+                    context.cluster(), input.search(), page, pageSize);
+        }
+        return listMessages(
+                context.cluster(), input.group(), startTime, endTime,
+                page, pageSize);
+    }
+
+    private DLQListOutput listGroups(String instanceId, String search, int page, int pageSize) {
+        PageResult<DLQGroupVO> result = dlqService.listDLQGroups(instanceId, search, page, pageSize);
+        return DLQListOutput.ofGroups(
+                instanceId,
+                result.getPage(),
+                result.getSize(),
+                result.getTotal(),
+                result.getItems().stream()
+                        .map(DLQListOutput.DLQGroupItem::from)
+                        .toList());
+    }
+
+    private DLQListOutput listMessages(String instanceId, String group, Long startTime, Long endTime,
+                                       int page, int pageSize) {
+        PageResult<DLQMessageVO> result = dlqService.listMessages(instanceId, group, startTime, endTime,
... 18471 lines suppressed ...