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 5158c0107 feat(server): address Studio instances by instanceId in the 
tool layer (#4308)
5158c0107 is described below

commit 5158c0107522ea13486cbe4e4494ccedfe78aa37
Author: lizhimins <[email protected]>
AuthorDate: Tue Sep 15 11:11:16 2026 +0800

    feat(server): address Studio instances by instanceId in the tool layer 
(#4308)
    
    The instance identifier becomes an explicit required parameter instead of
    an ambient target: the MCP header is x-rmq-instance-id,
    ToolExecutionContext and McpAuthentication expose instanceId(), the
    executor requires instanceId in the tool arguments and cross-checks it
    against the authenticated target, and the eleven platform-level tools
    skip both checks. Tool discovery keeps returning an empty list for a
    blank target so the console can still list the global tools.
---
 .../apache/rocketmq/studio/StudioApplication.java  |   2 -
 .../rocketmq/studio/common/config/CacheConfig.java |  10 +-
 .../rocketmq/studio/ops/ai/McpServerRegistry.java  |   2 +-
 .../rocketmq/studio/ops/ai/ToolController.java     |  26 +-
 .../studio/ops/ai/auth/McpAuthentication.java      |   4 +-
 .../studio/ops/ai/auth/McpAuthenticator.java       |  24 +-
 .../studio/ops/ai/tool/filter/ToolAuditFilter.java |   2 +-
 .../ops/ai/tool/filter/ToolCapabilityFilter.java   |   2 +-
 .../ops/ai/tool/filter/ToolMutationFilter.java     |   4 +-
 .../ops/ai/tool/service/ToolDiscoveryService.java  |  10 +-
 .../ops/ai/tool/service/ToolExecutionService.java  |  43 ++-
 .../ops/ai/tool/service/ToolTokenService.java      |   4 +-
 .../rocketmq/studio/StudioApplicationTest.java     |   5 +-
 .../studio/auth/AuthCorsIntegrationTest.java       |   8 +-
 .../rocketmq/studio/auth/AuthInterceptorTest.java  |   2 +-
 .../studio/common/config/CorsConfigTest.java       |   5 +-
 .../rocketmq/studio/ops/ai/ToolControllerTest.java |  79 ++++--
 .../ai/auth/McpCredentialAuthenticationTest.java   |  60 ++---
 .../studio/ops/ai/mcp/McpToolRegistrarTest.java    |  16 +-
 .../ops/ai/tool/filter/ToolMutationFilterTest.java |   8 +-
 .../ai/tool/service/ToolDiscoveryServiceTest.java  |  29 ++-
 .../tool/service/ToolExecutorInvocationTest.java   |  36 +--
 .../ai/tool/service/ToolInstanceRoutingTest.java   |  24 +-
 .../tool/service/ToolOutputSchemaContractTest.java | 290 ++++++++++++++++++---
 .../tool/service/ToolTokenConfigurationTest.java   |  16 +-
 .../ops/ai/tool/service/ToolTokenServiceTest.java  |  22 +-
 .../ops/alert/AlertSilenceControllerTest.java      |   3 +-
 web/src/api/ai.test.ts                             |  12 +-
 web/src/api/ai.ts                                  |   7 +-
 web/src/pages/ai/__tests__/AiPage.test.tsx         |  32 ++-
 web/src/pages/ai/index.tsx                         |   6 +-
 31 files changed, 550 insertions(+), 243 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/StudioApplication.java 
b/server/src/main/java/org/apache/rocketmq/studio/StudioApplication.java
index 1ea2baee9..38a537af1 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/StudioApplication.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/StudioApplication.java
@@ -19,12 +19,10 @@ package org.apache.rocketmq.studio;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.scheduling.annotation.EnableScheduling;
 
 @SpringBootApplication
 @EnableScheduling
-@EnableCaching
 public class StudioApplication {
     public static void main(String[] args) {
         SpringApplication.run(StudioApplication.class, args);
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/common/config/CacheConfig.java
 
b/server/src/main/java/org/apache/rocketmq/studio/common/config/CacheConfig.java
index 3fa534a07..cfb9f99d3 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/common/config/CacheConfig.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/common/config/CacheConfig.java
@@ -17,13 +17,18 @@
 package org.apache.rocketmq.studio.common.config;
 
 import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
 /**
- * Declares the {@link CacheManager} backing the {@code @Cacheable} methods 
enabled by
- * {@code @EnableCaching} on the application class.
+ * Enables caching and declares the {@link CacheManager} backing the {@code 
@Cacheable} methods.
+ * <p>
+ * {@code @EnableCaching} lives here rather than on {@code StudioApplication} 
so that
+ * {@code @WebMvcTest} slices (which do not load this {@code @Configuration}) 
never activate the
+ * caching infrastructure and therefore do not require a {@code CacheManager} 
bean to start their
+ * application context. The full application still picks up this class via 
component scanning.
  * <p>
  * {@code @EnableCaching} on its own leaves the manager to Spring Boot's cache
  * auto-configuration. In this application that did not yield a manager able 
to serve the
@@ -38,6 +43,7 @@ import org.springframework.context.annotation.Configuration;
  * reintroduce the same failure. Caches in use today: {@code data-sources}.
  */
 @Configuration
+@EnableCaching
 public class CacheConfig {
 
     @Bean
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/McpServerRegistry.java 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/McpServerRegistry.java
index cf947d5c7..56cca770f 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/McpServerRegistry.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/McpServerRegistry.java
@@ -24,7 +24,7 @@ public interface McpServerRegistry {
 
     List<AiToolVO> listTools();
 
-    List<AiToolVO> listTools(String clusterId);
+    List<AiToolVO> listTools(String instanceId);
 
     Object execute(String name, Map<String, Object> input);
 
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ToolController.java 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ToolController.java
index 9970b49ce..9c433a1a8 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ToolController.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/ToolController.java
@@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.rocketmq.studio.common.domain.Result;
+import org.apache.rocketmq.studio.ops.ai.tool.catalog.ToolCatalog;
 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;
@@ -31,6 +32,7 @@ import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -46,20 +48,34 @@ public class ToolController {
 
     @GetMapping
     public Result<List<AiToolVO>> listTools(
-            @RequestParam String cluster) {
-        return Result.ok(toolDiscoveryService.listTools(cluster));
+            @RequestParam(required = false) String instanceId,
+            @RequestParam(required = false) String cluster) {
+        return Result.ok(toolDiscoveryService.listTools(instanceId != null ? 
instanceId : cluster));
     }
 
     @PostMapping("/{name}/execute")
     public Result<Object> executeTool(
             @PathVariable String name,
+            @RequestParam String instanceId,
             @RequestBody(required = false) Map<String, Object> input) {
-        Map<String, Object> normalizedInput = input == null
-                ? Collections.emptyMap()
-                : input;
+        Map<String, Object> normalizedInput = withTargetInstance(name, input, 
instanceId);
         AiPayloadGuard.validateToolInvocation(name, normalizedInput, 
objectMapper);
         log.info("Executing registered AI tool: {}", name);
         return Result.ok(toolExecutor.execute(name, normalizedInput));
     }
 
+    /**
+     * The executor reads the Studio instance target from the tool arguments, 
so the REST query
+     * parameter is copied in. Platform-level tools are addressed by a 
physical {@code clusterName}
+     * and their input schemas reject unknown arguments, so their payload 
stays untouched.
+     */
+    private static Map<String, Object> withTargetInstance(
+            String name, Map<String, Object> input, String instanceId) {
+        Map<String, Object> normalized = new LinkedHashMap<>(input == null ? 
Map.of() : input);
+        if (!ToolCatalog.isInstanceIdExempt(name)) {
+            normalized.put(ToolCatalog.INSTANCE_ID_FIELD, instanceId);
+        }
+        return Collections.unmodifiableMap(normalized);
+    }
+
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthentication.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthentication.java
index b5911beb6..7f3dedaa3 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthentication.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/auth/McpAuthentication.java
@@ -17,13 +17,13 @@
 package org.apache.rocketmq.studio.ops.ai.auth;
 
 public record McpAuthentication(
-        String cluster,
+        String instanceId,
         String principal) {
 
     public static final String ATTRIBUTE = McpAuthentication.class.getName();
 
     public McpAuthentication {
-        requireText(cluster, "cluster");
+        requireText(instanceId, "instanceId");
         requireText(principal, "principal");
     }
 
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
index 5d65ccb62..bddc6f25c 100644
--- 
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
@@ -43,8 +43,8 @@ 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 HEADER_INSTANCE = "x-rmq-instance-id";
+    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(
@@ -75,13 +75,13 @@ public class McpAuthenticator {
 
     public McpAuthentication authenticate(HttpServletRequest request) {
         Authorization authorization = 
authorization(request.getHeader(HttpHeaders.AUTHORIZATION));
-        String cluster = requiredHeader(request, HEADER_CLUSTER);
+        String instanceId = requiredHeader(request, HEADER_INSTANCE);
         String timestamp = requiredHeader(request, HEADER_TIMESTAMP);
         verifyTimestamp(timestamp);
-        InstanceVO instance = resolveInstance(cluster);
+        InstanceVO instance = resolveInstance(instanceId);
         Credential credential = resolveCredential(instance);
-        verifySignature(request, authorization, credential, cluster, 
timestamp);
-        return new McpAuthentication(cluster, authorization.accessKey());
+        verifySignature(request, authorization, credential, instanceId, 
timestamp);
+        return new McpAuthentication(instanceId, authorization.accessKey());
     }
 
     private Authorization authorization(String value) {
@@ -115,9 +115,9 @@ public class McpAuthenticator {
         return value;
     }
 
-    private InstanceVO resolveInstance(String cluster) {
+    private InstanceVO resolveInstance(String instanceId) {
         try {
-            return 
instanceResolver.findByName(cluster).orElseThrow(McpAuthenticator::authenticationFailed);
+            return 
instanceResolver.findByName(instanceId).orElseThrow(McpAuthenticator::authenticationFailed);
         } catch (BusinessException exception) {
             if (exception.getCode() == 422) {
                 throw authenticationFailed(exception);
@@ -150,12 +150,12 @@ public class McpAuthenticator {
     }
 
     private static void verifySignature(HttpServletRequest request, 
Authorization authorization, Credential credential,
-                                 String cluster, String timestamp) {
+                                        String instanceId, String timestamp) {
         if (!authorization.accessKey().equals(credential.accessKey())) {
             throw authenticationFailed();
         }
         byte[] expected = hmac(credential.secretKey(),
-                canonicalRequest(authorization.accessKey(), cluster, timestamp,
+                canonicalRequest(authorization.accessKey(), instanceId, 
timestamp,
                         request.getMethod(), requestTarget(request)));
         if (!MessageDigest.isEqual(expected, authorization.signature())) {
             throw authenticationFailed();
@@ -193,14 +193,14 @@ public class McpAuthenticator {
 
     static String canonicalRequest(
             String accessKey,
-            String cluster,
+            String instanceId,
             String timestamp,
             String method,
             String requestTarget) {
         return String.join("\n",
                 ALGORITHM,
                 accessKey,
-                cluster,
+                instanceId,
                 timestamp,
                 method,
                 requestTarget);
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
index ed307f1b0..e80647ca3 100644
--- 
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
@@ -51,7 +51,7 @@ public class ToolAuditFilter implements ToolExecutionFilter {
         auditService.record(context.operationType(),
                 context.resourceType(),
                 context.definition().name(),
-                context.cluster(),
+                context.instanceId(),
                 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
index dd0b45e4b..198eb8910 100644
--- 
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
@@ -45,7 +45,7 @@ public class ToolCapabilityFilter implements 
ToolExecutionFilter {
             return chain.proceed(invocation);
         }
 
-        Set<String> capabilities = 
capabilityResolver.resolve(context.cluster());
+        Set<String> capabilities = 
capabilityResolver.resolve(context.instanceId());
         if (!capabilities.containsAll(definition.requiredCapabilities())) {
             throw 
ToolError.TOOL_CAPABILITY_UNSUPPORTED.exception(definition.name());
         }
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
index 12013ac1b..7c60d2887 100644
--- 
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
@@ -66,11 +66,11 @@ public class ToolMutationFilter implements 
ToolExecutionFilter {
         }
         if (context.dryRun()) {
             String token = tokenService.issue(context);
-            return new MutationOutput<>(MutationOutput.Status.PLANNED, 
context.cluster(), plan, token, null);
+            return new MutationOutput<>(MutationOutput.Status.PLANNED, 
context.instanceId(), plan, token, null);
         }
 
         Object result = chain.proceed(invocation);
-        return new MutationOutput<>(MutationOutput.Status.EXECUTED, 
context.cluster(), plan, null, result);
+        return new MutationOutput<>(MutationOutput.Status.EXECUTED, 
context.instanceId(), plan, null, result);
     }
 
     private void verifyL3Requirements(ToolExecutionContext context) {
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolDiscoveryService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolDiscoveryService.java
index 576a4f545..16b092d8d 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolDiscoveryService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolDiscoveryService.java
@@ -44,12 +44,12 @@ public class ToolDiscoveryService {
         this.instanceResolver = instanceResolver;
     }
 
-    public List<AiToolVO> listTools(String cluster) {
-        if (cluster == null || cluster.isBlank()) {
-            throw ToolError.TOOL_CLUSTER_REQUIRED.exception();
+    public List<AiToolVO> listTools(String instanceId) {
+        if (instanceId == null || instanceId.isBlank()) {
+            return List.of();
         }
-        InstanceVO instance = instanceResolver.findByName(cluster)
-                .orElseThrow(() -> 
ToolError.INSTANCE_NOT_FOUND.exception(cluster));
+        InstanceVO instance = instanceResolver.findByName(instanceId)
+                .orElseThrow(() -> 
ToolError.INSTANCE_NOT_FOUND.exception(instanceId));
         Set<String> capabilities = capabilityResolver.resolve(instance);
 
         return catalog.list().stream()
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolExecutionService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolExecutionService.java
index 78aeef68c..97ba10f5c 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolExecutionService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolExecutionService.java
@@ -94,21 +94,10 @@ public class ToolExecutionService {
     private Object executeInternal(String name, Map<String, Object> input, 
McpAuthentication authentication) {
         try {
             ToolDefinition definition = catalog.getDefinition(name);
-            Object value = input == null ? null : input.get("cluster");
-            if (!(value instanceof String cluster) || cluster.isBlank()) {
-                throw ToolError.TOOL_CLUSTER_REQUIRED.exception();
-            }
-            if (authentication != null) {
-                if (!cluster.equals(authentication.cluster())) {
-                    throw ToolError.TOOL_TARGET_MISMATCH.exception();
-                }
-            } else {
-                instanceResolver.findByName(cluster)
-                        .orElseThrow(() -> 
ToolError.INSTANCE_NOT_FOUND.exception(cluster));
-            }
+            String instanceId = resolveTargetInstance(definition, input, 
authentication);
             String caller = authentication == null
                     ? AuthenticatedUserContext.currentUsernameOrSystem() : 
authentication.principal();
-            ToolExecutionContext context = ToolExecutionContext.of(cluster, 
definition, input, caller);
+            ToolExecutionContext context = ToolExecutionContext.of(instanceId, 
definition, input, caller);
 
             ToolHandler<?, ?> handler = this.handlers.get(name);
             return filterChain.execute(new ToolInvocation(context, handler));
@@ -120,4 +109,32 @@ public class ToolExecutionService {
             throw internal;
         }
     }
+
+    /**
+     * Reads the Studio instance target from the tool arguments. 
Platform-level tools are addressed
+     * by a physical {@code clusterName} instead, so they skip both the 
mandatory {@code instanceId}
+     * check and the authenticated-target cross-check.
+     */
+    private String resolveTargetInstance(
+            ToolDefinition definition, Map<String, Object> input, 
McpAuthentication authentication) {
+        Object value = input == null ? null : 
input.get(ToolCatalog.INSTANCE_ID_FIELD);
+        if (!(value instanceof String instanceId) || instanceId.isBlank()) {
+            if (ToolCatalog.isInstanceIdExempt(definition.name())) {
+                return null;
+            }
+            throw 
ToolError.TOOL_INSTANCE_REQUIRED.exception(definition.name());
+        }
+        if (ToolCatalog.isInstanceIdExempt(definition.name())) {
+            return instanceId;
+        }
+        if (authentication != null) {
+            if (!instanceId.equals(authentication.instanceId())) {
+                throw ToolError.TOOL_TARGET_MISMATCH.exception();
+            }
+        } else {
+            instanceResolver.findByName(instanceId)
+                    .orElseThrow(() -> 
ToolError.INSTANCE_NOT_FOUND.exception(instanceId));
+        }
+        return instanceId;
+    }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolTokenService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolTokenService.java
index 6e2aacbea..02a239018 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolTokenService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolTokenService.java
@@ -124,7 +124,7 @@ public class ToolTokenService {
                     expiresAt,
                     context.definition().name(),
                     subjectBinding(context),
-                    context.cluster(),
+                    context.instanceId(),
                     canonicalInput(context.businessInput()));
             return objectMapper.writeValueAsBytes(payload);
         } catch (JsonProcessingException e) {
@@ -136,7 +136,7 @@ public class ToolTokenService {
         if (context.principal() != null && !context.principal().isBlank()) {
             return "principal:" + context.principal();
         }
-        return "instance:" + context.cluster();
+        return "instance:" + context.instanceId();
     }
 
     private static Map<String, Object> canonicalInput(Map<?, ?> input) {
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/StudioApplicationTest.java 
b/server/src/test/java/org/apache/rocketmq/studio/StudioApplicationTest.java
index 9a5b7a428..f22c26da1 100644
--- a/server/src/test/java/org/apache/rocketmq/studio/StudioApplicationTest.java
+++ b/server/src/test/java/org/apache/rocketmq/studio/StudioApplicationTest.java
@@ -16,8 +16,6 @@
  */
 package org.apache.rocketmq.studio;
 
-import org.apache.rocketmq.studio.ops.ai.tool.core.ToolExecutionException;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import org.apache.rocketmq.studio.ops.ai.tool.catalog.ToolCatalog;
 import org.apache.rocketmq.studio.ops.ai.tool.service.ToolDiscoveryService;
 import org.apache.rocketmq.studio.common.domain.enums.InstanceType;
@@ -75,8 +73,7 @@ class StudioApplicationTest {
     @Test
     void applicationContextLoadsWithInitializedDevSchema() throws Exception {
         assertThat(toolCatalog.list()).isNotEmpty();
-        assertThatThrownBy(() -> toolDiscoveryService.listTools(null))
-                .isInstanceOf(ToolExecutionException.class);
+        assertThat(toolDiscoveryService.listTools(null)).isEmpty();
         assertThat(instanceMapper.selectList(null)).isEmpty();
 
         mockMvc.perform(get("/api/instances"))
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthCorsIntegrationTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthCorsIntegrationTest.java
index c5760dd3f..d5ec1897f 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthCorsIntegrationTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthCorsIntegrationTest.java
@@ -34,6 +34,8 @@ import org.springframework.context.annotation.Import;
 import org.springframework.http.HttpHeaders;
 import org.springframework.test.web.servlet.MockMvc;
 
+import java.util.Optional;
+
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -112,13 +114,13 @@ class AuthCorsIntegrationTest {
                         .header(HttpHeaders.ORIGIN, FRONTEND_ORIGIN))
                 .andExpect(status().isUnauthorized());
 
-        verify(authService).isAuthenticated(null);
+        verify(authService).getAuthenticatedUser(null);
     }
     @Test
     void shouldRejectNonAdminMutationBeforeControllerExecution() throws 
Exception {
         String authorization = "Bearer reader-token";
-        when(authService.isAuthenticated(authorization)).thenReturn(true);
-        when(authService.isAdmin(authorization)).thenReturn(false);
+        
when(authService.getAuthenticatedUser(authorization)).thenReturn(Optional.of(
+                
LoginVO.UserInfo.builder().userId(2L).username("reader").admin(false).build()));
 
         mockMvc.perform(post("/api/instances/create")
                         .header(HttpHeaders.AUTHORIZATION, authorization)
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthInterceptorTest.java 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthInterceptorTest.java
index c25a2d110..042e15ce5 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthInterceptorTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthInterceptorTest.java
@@ -436,7 +436,7 @@ class AuthInterceptorTest {
     void shouldAllowReaderSafeAiToolExecutionForNonAdminUser() throws 
Exception {
         TestSession session = login(false);
         MockHttpServletRequest request = authenticatedRequest(
-                "POST", "/api/ai/tools/rmq.capabilities/execute", 
session.token());
+                "POST", "/api/ai/tools/rmq.instance.capabilities/execute", 
session.token());
 
         boolean allowed = session.interceptor().preHandle(
                 request, new MockHttpServletResponse(), new Object());
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/common/config/CorsConfigTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/common/config/CorsConfigTest.java
index 0f65a4e8d..c21c45453 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/common/config/CorsConfigTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/common/config/CorsConfigTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.rocketmq.studio.common.config;
 
+import org.apache.rocketmq.studio.WebMvcAuthTestSupport;
 import org.apache.rocketmq.studio.instance.dlq.DLQController;
 import org.apache.rocketmq.studio.instance.dlq.DLQExcelExportResultVO;
 import org.apache.rocketmq.studio.instance.dlq.DLQExportResultVO;
@@ -56,8 +57,8 @@ import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.
  */
 @WebMvcTest(value = DLQController.class, properties = 
"studio.cors.allowed-origins=http://localhost:5173";)
 @AutoConfigureMockMvc(addFilters = false)
-@Import(CorsConfig.class)
-class CorsConfigTest {
+@Import({CorsConfig.class, LegacyJackson2Config.class})
+class CorsConfigTest extends WebMvcAuthTestSupport {
 
     private static final String FRONTEND_ORIGIN = "http://localhost:5173";;
 
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/ToolControllerTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/ToolControllerTest.java
index 6e47ce806..8c1da9519 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/ToolControllerTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/ToolControllerTest.java
@@ -16,7 +16,6 @@
  */
 package org.apache.rocketmq.studio.ops.ai;
 
-import static org.mockito.Mockito.verifyNoInteractions;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.rocketmq.studio.ops.ai.auth.McpAuthentication;
 import org.apache.rocketmq.studio.ops.ai.tool.service.ToolDiscoveryService;
@@ -32,6 +31,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -66,7 +66,7 @@ class ToolControllerTest {
     }
 
     @Test
-    void listToolsReturnsCatalogEntries() throws Exception {
+    void listToolsReturnsCatalogEntriesTest() throws Exception {
         AiToolVO tool = AiToolVO.builder()
                 .name("rmq.cluster.list")
                 .version("1.0.0")
@@ -80,19 +80,19 @@ class ToolControllerTest {
                 .outputSchema(Map.of("type", "array"))
                 .viewHint("table")
                 .build();
-        
when(toolDiscoveryService.listTools("cluster-001")).thenReturn(List.of(tool));
+        
when(toolDiscoveryService.listTools("instance-id")).thenReturn(List.of(tool));
 
-        mockMvc.perform(get("/api/ai/tools").queryParam("cluster", 
"cluster-001"))
+        mockMvc.perform(get("/api/ai/tools").queryParam("instanceId", 
"instance-id"))
                 .andExpect(status().isOk())
                 
.andExpect(jsonPath("$.data[0].name").value("rmq.cluster.list"))
                 .andExpect(jsonPath("$.data[0].version").value("1.0.0"));
     }
 
     @Test
-    void listToolsDelegatesTheSelectedTarget() throws Exception {
+    void listToolsDelegatesTheSelectedTargetTest() throws Exception {
         when(toolDiscoveryService.listTools("cluster-001"))
                 .thenReturn(Collections.emptyList());
-        when(toolDiscoveryService.listTools("cluster-002"))
+        when(toolDiscoveryService.listTools("instance-id"))
                 .thenReturn(Collections.emptyList());
 
         mockMvc.perform(get("/api/ai/tools").queryParam("cluster", 
"cluster-001"))
@@ -103,38 +103,68 @@ class ToolControllerTest {
                 .andExpect(status().isOk());
 
         verify(toolDiscoveryService).listTools("cluster-001");
-        verify(toolDiscoveryService).listTools("cluster-002");
+        verify(toolDiscoveryService).listTools("instance-id");
+    }
+
+    /** The global-tool scope of the AI page lists platform tools without 
binding an Instance. */
+    @Test
+    void listToolsAcceptsAMissingTargetForPlatformToolsTest() throws Exception 
{
+        
when(toolDiscoveryService.listTools(null)).thenReturn(Collections.emptyList());
+
+        mockMvc.perform(get("/api/ai/tools"))
+                .andExpect(status().isOk());
+
+        verify(toolDiscoveryService).listTools(null);
     }
 
     @Test
-    void executeToolPreservesStructuredInputAndDottedName() throws Exception {
-        Map<String, Object> input = Map.of("cluster", "cluster-001");
+    void executeToolPreservesStructuredInputAndDottedNameTest() throws 
Exception {
+        Map<String, Object> input = Map.of("instanceId", "instance-id");
         Map<String, Object> output = Map.of(
-                "cluster", "cluster-001",
+                "instanceId", "instance-id",
                 "capabilities", List.of("REMOTING"));
-        when(toolExecutor.execute("rmq.capabilities", input))
+        when(toolExecutor.execute("rmq.instance.capabilities", input))
                 .thenReturn(output);
 
-        mockMvc.perform(post("/api/ai/tools/rmq.capabilities/execute")
+        mockMvc.perform(post("/api/ai/tools/rmq.instance.capabilities/execute")
+                        .queryParam("instanceId", "instance-id")
                         .contentType(MediaType.APPLICATION_JSON)
                         .content("""
-                                {"cluster":"cluster-001"}
+                                {"instanceId":"instance-id"}
                                 """))
                 .andExpect(status().isOk())
-                .andExpect(jsonPath("$.data.cluster").value("cluster-001"))
+                .andExpect(jsonPath("$.data.instanceId").value("instance-id"))
                 
.andExpect(jsonPath("$.data.capabilities[0]").value("REMOTING"));
 
-        verify(toolExecutor).execute("rmq.capabilities", input);
+        verify(toolExecutor).execute("rmq.instance.capabilities", input);
+    }
+
+    /** Platform tools are addressed by a physical clusterName, so the target 
stays out of their payload. */
+    @Test
+    void executeToolKeepsPlatformToolArgumentsUntouchedTest() throws Exception 
{
+        Map<String, Object> input = Map.of("clusterName", "DefaultCluster");
+        when(toolExecutor.execute("rmq.broker.list", input))
+                .thenReturn(Map.of("items", Collections.emptyList()));
+
+        mockMvc.perform(post("/api/ai/tools/rmq.broker.list/execute")
+                        .queryParam("instanceId", "")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("""
+                                {"clusterName":"DefaultCluster"}
+                                """))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.data.items").isArray());
+
+        verify(toolExecutor).execute("rmq.broker.list", input);
     }
 
     @Test
-    void callToolDelegatesAuthenticatedInput() throws Exception {
+    void callToolDelegatesAuthenticatedInputTest() throws Exception {
         Map<String, Object> input = Map.of(
-                "cluster", "cluster-001",
+                "instanceId", "instance-id",
                 "topic", "order-topic",
                 "dry_run", true);
-        when(toolExecutor.execute(
-                "rmq.topic.create", input, authentication))
+        when(toolExecutor.execute("rmq.topic.create", input, authentication))
                 .thenReturn(Map.of("status", "PLANNED"));
 
         mockMvc.perform(post("/api/mcp/tools/call")
@@ -144,7 +174,7 @@ class ToolControllerTest {
                                 {
                                   "name": "rmq.topic.create",
                                   "arguments": {
-                                    "cluster": "cluster-001",
+                                    "instanceId": "instance-id",
                                     "topic": "order-topic",
                                     "dry_run": true
                                   }
@@ -154,15 +184,8 @@ class ToolControllerTest {
                 .andExpect(jsonPath("$.data.status").value("PLANNED"));
 
         verify(toolExecutor).execute(
-                eq("rmq.topic.create"), 
org.mockito.ArgumentMatchers.argThat(arguments ->
+                eq("rmq.topic.create"), argThat(arguments ->
                         Boolean.TRUE.equals(arguments.get("dry_run"))), 
eq(authentication));
     }
 
-    @Test
-    void discoveryRequiresClusterEvenWhenLegacyInstanceIdIsProvided() throws 
Exception {
-        mockMvc.perform(get("/api/ai/tools").queryParam("instanceId", 
"instance-a"))
-                .andExpect(status().isBadRequest());
-        verifyNoInteractions(toolDiscoveryService);
-    }
-
 }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/auth/McpCredentialAuthenticationTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/auth/McpCredentialAuthenticationTest.java
index e4e5b866a..4634f0af0 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/auth/McpCredentialAuthenticationTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/auth/McpCredentialAuthenticationTest.java
@@ -68,7 +68,7 @@ import static org.mockito.Mockito.when;
 class McpCredentialAuthenticationTest {
 
     private static final List<String> ENTRY_POINTS = List.of("/api/mcp", 
"/api/mcp/tools/call");
-    private static final String CLUSTER = "instance-test";
+    private static final String INSTANCE_ID = "instance-test";
     private static final String CLOUD_ACCESS_KEY = "cloud-ak";
     private static final String CLOUD_SECRET_KEY = "cloud-test-secret";
     private static final String ADMIN_ACCESS_KEY = "admin-ak";
@@ -88,9 +88,9 @@ class McpCredentialAuthenticationTest {
         adminResolver = mock(RuntimeAdminClientResolver.class);
         cloudCredentialRepository = mock(CloudCredentialRepository.class);
 
-        instance = 
InstanceVO.builder().name(CLUSTER).vendor(InstanceVendor.ALIYUN)
+        instance = 
InstanceVO.builder().name(INSTANCE_ID).vendor(InstanceVendor.ALIYUN)
                 .credentialId(7L).adminCredentialRef("admin").build();
-        
when(instanceResolver.findByName(CLUSTER)).thenReturn(Optional.of(instance));
+        
when(instanceResolver.findByName(INSTANCE_ID)).thenReturn(Optional.of(instance));
 
         cloudCredential = new CloudCredentialVO();
         cloudCredential.setVendor(InstanceVendor.ALIYUN);
@@ -114,7 +114,7 @@ class McpCredentialAuthenticationTest {
 
     @ParameterizedTest
     @EnumSource(InstanceVendor.class)
-    void 
authenticatesResolvedInstanceCredentialsAtBothEntryPoints(InstanceVendor 
vendor) throws Exception {
+    void 
authenticatesResolvedInstanceCredentialsAtBothEntryPointsTest(InstanceVendor 
vendor) throws Exception {
         instance.setVendor(vendor);
         cloudCredential.setVendor(vendor);
         boolean apache = vendor == InstanceVendor.APACHE;
@@ -122,7 +122,7 @@ class McpCredentialAuthenticationTest {
         String secretKey = apache ? ADMIN_SECRET_KEY : CLOUD_SECRET_KEY;
 
         for (String path : ENTRY_POINTS) {
-            MockHttpServletRequest request = signedRequest(path, CLUSTER, 
accessKey, secretKey);
+            MockHttpServletRequest request = signedRequest(path, INSTANCE_ID, 
accessKey, secretKey);
             byte[] body = "body remains 
available".getBytes(StandardCharsets.UTF_8);
             request.setContent(body);
             MockHttpServletResponse response = new MockHttpServletResponse();
@@ -131,7 +131,7 @@ class McpCredentialAuthenticationTest {
             filter.doFilter(request, response, (verifiedRequest, 
verifiedResponse) -> {
                 invoked.set(true);
                 
assertThat(verifiedRequest.getAttribute(McpAuthentication.ATTRIBUTE))
-                        .isEqualTo(new McpAuthentication(CLUSTER, accessKey));
+                        .isEqualTo(new McpAuthentication(INSTANCE_ID, 
accessKey));
                 
assertThat(AuthenticatedUserContext.currentUsernameOrSystem()).isEqualTo(accessKey);
                 assertThat(AuthenticatedUserContext.currentUserId()).isNull();
                 
assertThat(AuthenticatedUserContext.currentUserIsAdminOrSystem()).isFalse();
@@ -142,7 +142,7 @@ class McpCredentialAuthenticationTest {
             assertThat(invoked).isTrue();
             assertIdentityCleared();
         }
-        verify(instanceResolver, 
times(ENTRY_POINTS.size())).findByName(CLUSTER);
+        verify(instanceResolver, 
times(ENTRY_POINTS.size())).findByName(INSTANCE_ID);
         verifyNoMoreInteractions(instanceResolver);
         if (apache) {
             verify(adminResolver, 
times(ENTRY_POINTS.size())).resolveCredential(instance);
@@ -156,11 +156,11 @@ class McpCredentialAuthenticationTest {
     }
 
     @Test
-    void clearsIdentityAndPropagatesDownstreamFailure() throws Exception {
+    void clearsIdentityAndPropagatesDownstreamFailureTest() throws Exception {
         ServletException failure = new ServletException("Downstream execution 
failed");
 
         for (String path : ENTRY_POINTS) {
-            MockHttpServletRequest request = signedRequest(path, CLUSTER, 
CLOUD_ACCESS_KEY, CLOUD_SECRET_KEY);
+            MockHttpServletRequest request = signedRequest(path, INSTANCE_ID, 
CLOUD_ACCESS_KEY, CLOUD_SECRET_KEY);
             MockHttpServletResponse response = new MockHttpServletResponse();
             assertThatThrownBy(() -> filter.doFilter(request, response, 
(verifiedRequest, verifiedResponse) -> {
                 
assertThat(AuthenticatedUserContext.currentUsernameOrSystem()).isEqualTo(CLOUD_ACCESS_KEY);
@@ -175,15 +175,15 @@ class McpCredentialAuthenticationTest {
     }
 
     @Test
-    void rejectsUnknownTargetBeforeReadingCredentials() throws Exception {
-        
when(instanceResolver.findByName(CLUSTER)).thenReturn(Optional.empty());
+    void rejectsUnknownTargetBeforeReadingCredentialsTest() throws Exception {
+        
when(instanceResolver.findByName(INSTANCE_ID)).thenReturn(Optional.empty());
 
         assertRejectedAtBothEntryPoints(401);
         verifyNoInteractions(adminResolver, cloudCredentialRepository);
     }
 
     @Test
-    void rejectsMissingCloudCredentialReference() throws Exception {
+    void rejectsMissingCloudCredentialReferenceTest() throws Exception {
         instance.setCredentialId(null);
 
         assertRejectedAtBothEntryPoints(401);
@@ -191,7 +191,7 @@ class McpCredentialAuthenticationTest {
     }
 
     @Test
-    void rejectsMissingCloudCredential() throws Exception {
+    void rejectsMissingCloudCredentialTest() throws Exception {
         
when(cloudCredentialRepository.findById(7L)).thenReturn(Optional.empty());
 
         assertRejectedAtBothEntryPoints(401);
@@ -199,7 +199,7 @@ class McpCredentialAuthenticationTest {
 
     @ParameterizedTest
     @ValueSource(ints = {422, 503})
-    void rejectsApacheCredentialBusinessFailures(int code) throws Exception {
+    void rejectsApacheCredentialBusinessFailuresTest(int code) throws 
Exception {
         instance.setVendor(InstanceVendor.APACHE);
         when(adminResolver.resolveCredential(instance))
                 .thenThrow(new BusinessException(code, "Admin credential 
unavailable"));
@@ -208,8 +208,8 @@ class McpCredentialAuthenticationTest {
     }
 
     @Test
-    void rejectsCredentialConfigurationFailureDuringTargetResolution() throws 
Exception {
-        when(instanceResolver.findByName(CLUSTER))
+    void rejectsCredentialConfigurationFailureDuringTargetResolutionTest() 
throws Exception {
+        when(instanceResolver.findByName(INSTANCE_ID))
                 .thenThrow(new BusinessException(422, "Admin credential is not 
configured"));
 
         assertRejectedAtBothEntryPoints(401);
@@ -217,8 +217,8 @@ class McpCredentialAuthenticationTest {
     }
 
     @Test
-    void preservesTargetResolutionFailureAsInternalError() throws Exception {
-        when(instanceResolver.findByName(CLUSTER))
+    void preservesTargetResolutionFailureAsInternalErrorTest() throws 
Exception {
+        when(instanceResolver.findByName(INSTANCE_ID))
                 .thenThrow(new BusinessException(503, "NameServer 
unavailable"));
 
         assertRejectedAtBothEntryPoints(500);
@@ -226,7 +226,7 @@ class McpCredentialAuthenticationTest {
     }
 
     @Test
-    void preservesCloudCredentialRepositoryFailureAsInternalError() throws 
Exception {
+    void preservesCloudCredentialRepositoryFailureAsInternalErrorTest() throws 
Exception {
         when(cloudCredentialRepository.findById(7L))
                 .thenThrow(new DataAccessResourceFailureException("Credential 
database unavailable"));
 
@@ -234,7 +234,7 @@ class McpCredentialAuthenticationTest {
     }
 
     @Test
-    void preservesUnexpectedAdminResolverFailureAsInternalError() throws 
Exception {
+    void preservesUnexpectedAdminResolverFailureAsInternalErrorTest() throws 
Exception {
         instance.setVendor(InstanceVendor.APACHE);
         when(adminResolver.resolveCredential(instance))
                 .thenThrow(new DataAccessResourceFailureException("Credential 
resolver unavailable"));
@@ -247,8 +247,8 @@ class McpCredentialAuthenticationTest {
         "wrong-access-key, cloud-test-secret",
         "cloud-ak, wrong-secret"
     })
-    void rejectsInvalidCredentials(String accessKey, String secretKey) throws 
Exception {
-        assertRejectedAtBothEntryPoints(401, CLUSTER, accessKey, secretKey);
+    void rejectsInvalidCredentialsTest(String accessKey, String secretKey) 
throws Exception {
+        assertRejectedAtBothEntryPoints(401, INSTANCE_ID, accessKey, 
secretKey);
     }
 
     @Nested
@@ -276,7 +276,7 @@ class McpCredentialAuthenticationTest {
         }
 
         @Test
-        void 
authenticatesConfiguredTargetWithoutDatabaseRecordAtBothEntryPoints() throws 
Exception {
+        void 
authenticatesConfiguredTargetWithoutDatabaseRecordAtBothEntryPointsTest() 
throws Exception {
             for (String path : ENTRY_POINTS) {
                 MockHttpServletRequest request = signedRequest(
                         path, CONFIGURED_CLUSTER, ADMIN_ACCESS_KEY, 
ADMIN_SECRET_KEY);
@@ -301,7 +301,7 @@ class McpCredentialAuthenticationTest {
 
         @ParameterizedTest
         @EnumSource(InstanceVendor.class)
-        void 
registeredTargetWithMissingCredentialsNeverFallsBackToConfiguration(InstanceVendor
 vendor)
+        void 
registeredTargetWithMissingCredentialsNeverFallsBackToConfigurationTest(InstanceVendor
 vendor)
                 throws Exception {
             instance.setName(CONFIGURED_CLUSTER);
             instance.setVendor(vendor);
@@ -318,12 +318,12 @@ class McpCredentialAuthenticationTest {
 
     private void assertRejectedAtBothEntryPoints(int status) throws Exception {
         boolean apache = instance.getVendor() == InstanceVendor.APACHE;
-        assertRejectedAtBothEntryPoints(status, CLUSTER,
+        assertRejectedAtBothEntryPoints(status, INSTANCE_ID,
                 apache ? ADMIN_ACCESS_KEY : CLOUD_ACCESS_KEY,
                 apache ? ADMIN_SECRET_KEY : CLOUD_SECRET_KEY);
     }
 
-    private void assertRejectedAtBothEntryPoints(int status, String cluster, 
String accessKey, String secretKey)
+    private void assertRejectedAtBothEntryPoints(int status, String 
instanceId, String accessKey, String secretKey)
             throws Exception {
         ObjectMapper objectMapper = new ObjectMapper();
         Map<String, String> expectedError = status == 401
@@ -334,7 +334,7 @@ class McpCredentialAuthenticationTest {
                         "message", "MCP authentication failed unexpectedly.",
                         "hint", "Retry once; if the failure persists, contact 
an administrator.");
         for (String path : ENTRY_POINTS) {
-            MockHttpServletRequest request = signedRequest(path, cluster, 
accessKey, secretKey);
+            MockHttpServletRequest request = signedRequest(path, instanceId, 
accessKey, secretKey);
             MockHttpServletResponse response = new MockHttpServletResponse();
             AtomicBoolean invoked = new AtomicBoolean();
 
@@ -357,17 +357,17 @@ class McpCredentialAuthenticationTest {
         
assertThat(AuthenticatedUserContext.currentUserIsAdminOrSystem()).isTrue();
     }
 
-    private MockHttpServletRequest signedRequest(String path, String cluster, 
String accessKey, String secretKey)
+    private MockHttpServletRequest signedRequest(String path, String 
instanceId, String accessKey, String secretKey)
             throws Exception {
         String timestamp = Long.toString(System.currentTimeMillis());
-        String canonical = McpAuthenticator.canonicalRequest(accessKey, 
cluster, timestamp, "POST", path);
+        String canonical = McpAuthenticator.canonicalRequest(accessKey, 
instanceId, timestamp, "POST", path);
         Mac mac = Mac.getInstance("HmacSHA256");
         mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), 
"HmacSHA256"));
         String signature = 
HexFormat.of().formatHex(mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8)));
         MockHttpServletRequest request = new MockHttpServletRequest("POST", 
path);
         request.addHeader(HttpHeaders.AUTHORIZATION,
                 McpAuthenticator.ALGORITHM + " Credential=" + accessKey + ", 
Signature=" + signature);
-        request.addHeader(McpAuthenticator.HEADER_CLUSTER, cluster);
+        request.addHeader(McpAuthenticator.HEADER_INSTANCE, instanceId);
         request.addHeader(McpAuthenticator.HEADER_TIMESTAMP, timestamp);
         return request;
     }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/mcp/McpToolRegistrarTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/mcp/McpToolRegistrarTest.java
index e35d4358e..4344af62c 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/mcp/McpToolRegistrarTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/mcp/McpToolRegistrarTest.java
@@ -48,7 +48,7 @@ class McpToolRegistrarTest {
     void propagatesTransportAuthenticationToToolExecutor() {
         ToolDefinition definition = toolDefinition();
         ToolExecutionService toolExecutor = mock(ToolExecutionService.class);
-        Map<String, Object> output = Map.of("cluster", "cluster-001");
+        Map<String, Object> output = Map.of("instanceId", "cluster-001");
         when(toolExecutor.execute(
                 same(definition.name()), any(), same(AUTHENTICATION)))
                 .thenReturn(output);
@@ -59,7 +59,7 @@ class McpToolRegistrarTest {
                 exchange(AUTHENTICATION),
                 new McpSchema.CallToolRequest(
                         definition.name(), Map.of(
-                                "cluster", "cluster-001",
+                                "instanceId", "cluster-001",
                                 "dry_run", true)));
 
         ArgumentCaptor<Map<String, Object>> captor = 
ArgumentCaptor.forClass(Map.class);
@@ -70,7 +70,7 @@ class McpToolRegistrarTest {
         assertThat(result.structuredContent()).isEqualTo(output);
         
assertThat(specification.tool().inputSchema()).isEqualTo(definition.inputSchema());
         assertThat(specification.tool().description())
-                .isEqualTo("Create a RocketMQ topic\nRequires all 
capabilities: TOPIC_MANAGEMENT.");
+                .isEqualTo("Update a RocketMQ topic\nRequires all 
capabilities: TOPIC_MANAGEMENT.");
         
assertThat(specification.tool().annotations().destructiveHint()).isFalse();
     }
 
@@ -86,16 +86,16 @@ class McpToolRegistrarTest {
 
     private static ToolDefinition toolDefinition() {
         return new ToolDefinition(
-                "rmq.topic.create",
-                new ToolDefinition.Cli("topic", "create"),
-                "Create a RocketMQ topic",
+                "rmq.topic.update",
+                new ToolDefinition.Cli("topic", "update"),
+                "Update a RocketMQ topic",
                 ToolRiskLevel.L2,
                 "topic:write",
                 List.of("TOPIC_MANAGEMENT"),
                 Map.of(
                         "type", "object",
-                        "properties", Map.of("cluster", Map.of("type", 
"string")),
-                        "required", List.of("cluster"),
+                        "properties", Map.of("instanceId", Map.of("type", 
"string")),
+                        "required", List.of("instanceId"),
                         "additionalProperties", false),
                 Map.of("type", "object", "additionalProperties", true),
                 "json",
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolMutationFilterTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolMutationFilterTest.java
index bc521c43b..a8e2363d0 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolMutationFilterTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/filter/ToolMutationFilterTest.java
@@ -116,7 +116,7 @@ class ToolMutationFilterTest {
     @Test
     void invalidTokenPreventsPlanGenerationAndExecution() {
         ToolExecutionContext context = context(ToolRiskLevel.L2, 
Map.of("topic", "orders", "confirm_token", "invalid"));
-        var failure = 
ToolError.CONFIRMATION_TOKEN_INVALID.exception("rmq.topic.create");
+        var failure = 
ToolError.CONFIRMATION_TOKEN_INVALID.exception("rmq.topic.update");
         doThrow(failure).when(tokens).verify(context);
 
         assertThatThrownBy(() -> chain.execute(new ToolInvocation(context, 
handler))).isSameAs(failure);
@@ -125,8 +125,8 @@ class ToolMutationFilterTest {
     }
 
     private static ToolExecutionContext context(ToolRiskLevel risk, 
Map<String, Object> input) {
-        ToolDefinition definition = new ToolDefinition("rmq.topic.create",
-                new ToolDefinition.Cli("topic", "create"), "Create topic", 
risk,
+        ToolDefinition definition = new ToolDefinition("rmq.topic.update",
+                new ToolDefinition.Cli("topic", "update"), "Create topic", 
risk,
                 "topic:write", List.of(), Map.of(), Map.of(), null, false, 
null);
         return ToolExecutionContext.of("instance-a", definition, input, 
"alice");
     }
@@ -145,7 +145,7 @@ class ToolMutationFilterTest {
 
         @Override
         public String name() {
-            return "rmq.topic.create";
+            return "rmq.topic.update";
         }
 
         @Override
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolDiscoveryServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolDiscoveryServiceTest.java
index 0ee8758c9..882af7c46 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolDiscoveryServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolDiscoveryServiceTest.java
@@ -73,7 +73,7 @@ class ToolDiscoveryServiceTest {
     }
 
     @Test
-    void discoveryWithInstanceExposesSupportedTools() {
+    void discoveryWithInstanceExposesSupportedToolsTest() {
         when(instanceRepository.findByIdentifier("instance-a"))
                 .thenReturn(Optional.of(InstanceVO.builder()
                         .name("instance-a")
@@ -85,16 +85,16 @@ class ToolDiscoveryServiceTest {
                 .extracting(AiToolVO::getName)
                 .contains(
                         "rmq.cluster.list",
-                        "rmq.capabilities",
+                        "rmq.instance.capabilities",
                         "rmq.dashboard.summary",
                         "rmq.topic.list",
                         "rmq.topic.route",
-                        "rmq.topic.send",
+                        "rmq.message.send",
                         "rmq.group.list",
                         "rmq.group.reset_offset",
                         "rmq.alert.rule.list")
-                .contains("rmq.nameserver.config.diff")
-                .doesNotContain("rmq.proxy.config_update", 
"rmq.lite_topic.list", "rmq.lite_topic.create");
+                .contains("rmq.nameserver.config")
+                .doesNotContain("rmq.proxy.config_update");
     }
 
     @ParameterizedTest
@@ -114,17 +114,20 @@ class ToolDiscoveryServiceTest {
 
         var tools = 
discoveryService.listTools("instance-a").stream().map(AiToolVO::getName).toList();
         assertThat(tools)
-                .contains("rmq.capabilities", "rmq.topic.list", 
"rmq.topic.create", "rmq.group.list",
+                .contains("rmq.instance.capabilities", "rmq.topic.list", 
"rmq.topic.update", "rmq.group.list",
                         "rmq.acl.list", "rmq.user.list")
-                .doesNotContain("rmq.cluster.list", "rmq.broker.list", 
"rmq.nameserver.config.diff", "rmq.dlq.list",
-                        "rmq.topic.route", "rmq.topic.send", 
"rmq.group.reset_offset",
-                        "rmq.lite_topic.list", "rmq.lite_topic.create");
+                .doesNotContain("rmq.cluster.list", "rmq.broker.list", 
"rmq.nameserver.config", "rmq.group.dlq_list",
+                        "rmq.topic.route", "rmq.message.send", 
"rmq.group.reset_offset");
     }
+    /** The AI page lists platform tools without binding an Instance, so a 
missing target is not an error. */
     @Test
-    void discoveryRejectsMissingOrUnregisteredNames() {
-        assertThatThrownBy(() -> discoveryService.listTools(" "))
-                .isInstanceOfSatisfying(ToolExecutionException.class,
-                        error -> assertThat(error.getCode()).isEqualTo(400));
+    void discoveryWithoutATargetExposesNoToolsTest() {
+        assertThat(discoveryService.listTools(null)).isEmpty();
+        assertThat(discoveryService.listTools(" ")).isEmpty();
+    }
+
+    @Test
+    void discoveryRejectsAnUnregisteredInstanceTest() {
         assertThatThrownBy(() -> discoveryService.listTools("unknown"))
                 .isInstanceOfSatisfying(ToolExecutionException.class,
                         error -> assertThat(error.getCode()).isEqualTo(404));
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolExecutorInvocationTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolExecutorInvocationTest.java
index ff43d722d..2a4d759a0 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolExecutorInvocationTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolExecutorInvocationTest.java
@@ -79,22 +79,22 @@ class ToolExecutorInvocationTest {
     }
 
     @Test
-    void capturesConsoleUserBeforeEnteringChain() {
+    void capturesConsoleUserBeforeEnteringChainTest() {
         AuthenticatedUserContext.setUsername("alice");
         when(chain.execute(any(ToolInvocation.class))).thenAnswer(invocation 
-> {
             AuthenticatedUserContext.setUsername("bob");
             return null;
         });
-        executor.execute("rmq.topic.list", Map.of("cluster", "instance-a"));
+        executor.execute("rmq.topic.list", Map.of("instanceId", "instance-a"));
         ArgumentCaptor<ToolInvocation> invocation = 
ArgumentCaptor.forClass(ToolInvocation.class);
         verify(chain).execute(invocation.capture());
         
assertThat(invocation.getValue().context().principal()).isEqualTo("alice");
     }
 
     @Test
-    void passesBoundInstancePrincipalAndHandlerToChain() {
+    void passesBoundInstancePrincipalAndHandlerToChainTest() {
         AuthenticatedUserContext.setUsername("console-user");
-        Map<String, Object> input = Map.of("cluster", "instance-a");
+        Map<String, Object> input = Map.of("instanceId", "instance-a");
         Map<String, Object> output = Map.of("items", List.of());
         when(chain.execute(any(ToolInvocation.class))).thenReturn(output);
 
@@ -105,16 +105,16 @@ class ToolExecutorInvocationTest {
         assertThat(invocation.getValue().handler()).isSameAs(handler);
         
assertThat(invocation.getValue().context().definition()).isSameAs(definition);
         
assertThat(invocation.getValue().context().input()).containsExactlyEntriesOf(input);
-        
assertThat(invocation.getValue().context().cluster()).isEqualTo("instance-a");
+        
assertThat(invocation.getValue().context().instanceId()).isEqualTo("instance-a");
         
assertThat(invocation.getValue().context().principal()).isEqualTo("mcp-access-key");
         assertThat(result).isSameAs(output);
         verifyNoInteractions(instances);
     }
 
     @Test
-    void rejectsMissingBlankAndNonStringClusterBeforeFilters() {
+    void rejectsMissingBlankAndNonStringInstanceIdBeforeFiltersTest() {
         for (Map<String, Object> input : List.<Map<String, Object>>of(
-                Map.of(), Map.of("cluster", " "), Map.of("cluster", 1), 
Map.of("cluster", true))) {
+                Map.of(), Map.of("instanceId", " "), Map.of("instanceId", 1), 
Map.of("instanceId", true))) {
             assertThatThrownBy(() -> executor.execute("rmq.topic.list", input))
                     .isInstanceOfSatisfying(ToolExecutionException.class,
                             error -> 
assertThat(error.getCode()).isEqualTo(400));
@@ -126,8 +126,8 @@ class ToolExecutorInvocationTest {
     }
 
     @Test
-    void bodyClusterRequiresARegisteredNameAndNeverFallsBackToNumericId() {
-        assertThatThrownBy(() -> executor.execute("rmq.topic.list", 
Map.of("cluster", "1")))
+    void 
bodyInstanceIdRequiresARegisteredNameAndNeverFallsBackToNumericIdTest() {
+        assertThatThrownBy(() -> executor.execute("rmq.topic.list", 
Map.of("instanceId", "1")))
                 .isInstanceOfSatisfying(ToolExecutionException.class,
                         error -> assertThat(error.getCode()).isEqualTo(404));
         verify(instances).findByName("1");
@@ -136,39 +136,39 @@ class ToolExecutorInvocationTest {
     }
 
     @Test
-    void rejectsDifferentAuthenticatedInstanceBeforeFilters() {
+    void rejectsDifferentAuthenticatedInstanceBeforeFiltersTest() {
         InstanceVO other = InstanceVO.builder().name("instance-b").build();
         other.setId(2L);
         
when(instances.findByName("instance-b")).thenReturn(Optional.of(other));
         assertThatThrownBy(() -> executor.execute("rmq.topic.list",
-                Map.of("cluster", "instance-a"), new 
McpAuthentication("instance-b", "shared-access-key")))
+                Map.of("instanceId", "instance-a"), new 
McpAuthentication("instance-b", "shared-access-key")))
                 .isInstanceOfSatisfying(ToolExecutionException.class,
                         error -> assertThat(error.getCode()).isEqualTo(403));
         verifyNoInteractions(chain);
     }
 
     @Test
-    void rejectsDifferentAuthenticatedClusterWithoutResolvingEitherName() {
+    void rejectsDifferentAuthenticatedTargetWithoutResolvingEitherNameTest() {
         assertThatThrownBy(() -> executor.execute("rmq.topic.list",
-                Map.of("cluster", "instance-a"), new McpAuthentication("1", 
"access-key")))
+                Map.of("instanceId", "instance-a"), new McpAuthentication("1", 
"access-key")))
                 .isInstanceOfSatisfying(ToolExecutionException.class,
                         error -> assertThat(error.getCode()).isEqualTo(403));
         verifyNoInteractions(instances, chain);
     }
 
     @Test
-    void acceptsAuthenticatedConfiguredClusterWithoutDatabaseIdentity() {
-        executor.execute("rmq.topic.list", Map.of("cluster", "DefaultCluster"),
+    void acceptsAuthenticatedConfiguredClusterWithoutDatabaseIdentityTest() {
+        executor.execute("rmq.topic.list", Map.of("instanceId", 
"DefaultCluster"),
                 new McpAuthentication("DefaultCluster", "access-key"));
         ArgumentCaptor<ToolInvocation> invocation = 
ArgumentCaptor.forClass(ToolInvocation.class);
         verify(chain).execute(invocation.capture());
-        
assertThat(invocation.getValue().context().cluster()).isEqualTo("DefaultCluster");
+        
assertThat(invocation.getValue().context().instanceId()).isEqualTo("DefaultCluster");
         verifyNoInteractions(instances);
     }
 
     @Test
-    void missingMcpAuthenticationDoesNotBecomeAConsoleCall() {
-        assertThatThrownBy(() -> executor.execute("rmq.topic.list", 
Map.of("cluster", "instance-a"), null))
+    void missingMcpAuthenticationDoesNotBecomeAConsoleCallTest() {
+        assertThatThrownBy(() -> executor.execute("rmq.topic.list", 
Map.of("instanceId", "instance-a"), null))
                 .isInstanceOf(ToolExecutionException.class);
         verifyNoInteractions(instances, chain);
     }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolInstanceRoutingTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolInstanceRoutingTest.java
index 7f32f7d6b..b30b997e4 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolInstanceRoutingTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolInstanceRoutingTest.java
@@ -20,6 +20,7 @@ import org.apache.rocketmq.studio.instance.InstanceResolver;
 import 
org.apache.rocketmq.studio.provider.apache.RocketMQDefaultClusterResolver;
 
 import org.apache.rocketmq.studio.audit.OperationAuditService;
+import org.apache.rocketmq.studio.cluster.broker.RuntimeAdminClientResolver;
 import org.apache.rocketmq.studio.common.domain.enums.InstanceType;
 import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 import org.apache.rocketmq.studio.instance.InstanceRepository;
@@ -60,32 +61,33 @@ class ToolInstanceRoutingTest {
 
     @ParameterizedTest
     @CsvSource({"APACHE,false", "APACHE,true", "ALIYUN,false", 
"TENCENT,false"})
-    void 
bodyClusterRoutesCapabilitiesMetadataAndAuditToTheSameTarget(InstanceVendor 
vendor, boolean configured) {
-        String cluster = configured ? "DefaultCluster" : "prod-orders";
+    void 
bodyInstanceIdRoutesCapabilitiesMetadataAndAuditToTheSameTargetTest(InstanceVendor
 vendor, boolean configured) {
+        String target = configured ? "DefaultCluster" : "prod-orders";
         InstanceRepository instances = mock(InstanceRepository.class);
         RocketMQDefaultClusterResolver configuredClusters = 
mock(RocketMQDefaultClusterResolver.class);
         InstanceResolver targets = new InstanceResolver(instances, 
configuredClusters);
-        InstanceVO instance = InstanceVO.builder().name(cluster).vendor(vendor)
+        InstanceVO instance = InstanceVO.builder().name(target).vendor(vendor)
                 .type(vendor == InstanceVendor.APACHE ? 
InstanceType.PROXY_CLUSTER : InstanceType.CLOUD)
                 
.endpoint("selected-ns:9876").cloudInstanceId("vendor-resource-id").build();
         if (configured) {
-            
when(configuredClusters.find(cluster)).thenReturn(Optional.of(instance));
+            
when(configuredClusters.find(target)).thenReturn(Optional.of(instance));
         } else {
             instance.setId(7L);
-            
when(instances.findByName(cluster)).thenReturn(Optional.of(instance));
-            
when(instances.findByIdentifier(cluster)).thenReturn(Optional.of(instance));
+            
when(instances.findByName(target)).thenReturn(Optional.of(instance));
+            
when(instances.findByIdentifier(target)).thenReturn(Optional.of(instance));
         }
         InstanceProvider selected = mock(InstanceProvider.class);
         when(selected.vendor()).thenReturn(vendor);
         
when(selected.capabilities()).thenReturn(Set.of(InstanceCapability.TOPIC_MANAGEMENT));
-        when(selected.listTopics(cluster, null, null)).thenReturn(List.of());
+        when(selected.listTopics(target, null, null)).thenReturn(List.of());
         InstanceProviderRegistry registry = new 
InstanceProviderRegistry(List.of(selected),
                 List.of(),
                 targets);
         MetadataProvider globalMetadata = mock(MetadataProvider.class);
         AdminClient globalAdmin = mock(AdminClient.class);
         MetadataService metadata = new MetadataService(globalMetadata, 
globalAdmin, registry, targets,
-                mock(OperationAuditService.class), mock(MessageService.class));
+                mock(OperationAuditService.class), mock(MessageService.class),
+                mock(RuntimeAdminClientResolver.class));
         TopicListToolHandler handler = new TopicListToolHandler(metadata);
         ToolDefinition definition = new ToolCatalog(new 
DefaultResourceLoader()).getDefinition(handler.name());
         ToolCatalog catalog = mock(ToolCatalog.class);
@@ -101,11 +103,11 @@ class ToolInstanceRoutingTest {
                 filters,
                 targets);
 
-        executor.execute(handler.name(), Map.of("cluster", cluster));
+        executor.execute(handler.name(), Map.of(ToolCatalog.INSTANCE_ID_FIELD, 
target));
 
         verify(selected).capabilities();
-        verify(selected).listTopics(cluster, null, null);
-        verify(audit).record(anyString(), anyString(), eq(handler.name()), 
eq(cluster), isNull(), eq("SUCCESS"));
+        verify(selected).listTopics(target, null, null);
+        verify(audit).record(anyString(), anyString(), eq(handler.name()), 
eq(target), isNull(), eq("SUCCESS"));
         verifyNoInteractions(globalMetadata, globalAdmin);
     }
 }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolOutputSchemaContractTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolOutputSchemaContractTest.java
index 974035c71..6cfee313d 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolOutputSchemaContractTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolOutputSchemaContractTest.java
@@ -17,72 +17,300 @@
 package org.apache.rocketmq.studio.ops.ai.tool.service;
 
 import org.apache.rocketmq.studio.cluster.broker.BrokerVO;
-import org.apache.rocketmq.studio.cluster.client.ClientConnectionVO;
+import org.apache.rocketmq.studio.cluster.metrics.MetricDataVO;
 import org.apache.rocketmq.studio.cluster.proxy.ProxyVO;
+import org.apache.rocketmq.studio.common.config.LegacyJackson2Config;
 import org.apache.rocketmq.studio.common.domain.enums.BrokerStatus;
-import org.apache.rocketmq.studio.common.domain.enums.ClientLanguage;
-import org.apache.rocketmq.studio.common.domain.enums.ClientType;
 import org.apache.rocketmq.studio.common.domain.enums.ClusterStatus;
-import org.apache.rocketmq.studio.common.domain.enums.Protocol;
+import org.apache.rocketmq.studio.common.domain.enums.ConsumeType;
+import org.apache.rocketmq.studio.common.domain.enums.SubscriptionMode;
+import org.apache.rocketmq.studio.common.domain.enums.TopicPerm;
+import org.apache.rocketmq.studio.common.domain.enums.TopicType;
+import org.apache.rocketmq.studio.instance.acl.AclRuleVO;
 import org.apache.rocketmq.studio.ops.ai.tool.catalog.ToolCatalog;
-import 
org.apache.rocketmq.studio.ops.ai.tool.contract.common.ClusterListOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclRuleItem;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.acl.AclUserItem;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.alert.AlertRuleListItem;
+import 
org.apache.rocketmq.studio.ops.ai.tool.contract.broker.BrokerConfigOutput;
+import 
org.apache.rocketmq.studio.ops.ai.tool.contract.broker.BrokerDescribeOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.cluster.ClusterListItem;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.ListOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.MutationOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.common.PageOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.group.GroupDetailOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.group.GroupListItem;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.group.ResetOffsetOutput;
+import 
org.apache.rocketmq.studio.ops.ai.tool.contract.instance.InstanceCapabilitiesOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.message.MessageItem;
+import 
org.apache.rocketmq.studio.ops.ai.tool.contract.message.MessageQueryDlqOutput;
+import 
org.apache.rocketmq.studio.ops.ai.tool.contract.message.MessageRedeliveryDlqOutput;
+import 
org.apache.rocketmq.studio.ops.ai.tool.contract.message.MessageRedeliveryOutput;
+import 
org.apache.rocketmq.studio.ops.ai.tool.contract.message.MessageSendOutput;
+import 
org.apache.rocketmq.studio.ops.ai.tool.contract.message.MessageTraceOutput;
+import 
org.apache.rocketmq.studio.ops.ai.tool.contract.nameserver.NameserverConfigItem;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.ops.AuditItem;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.proxy.ProxyConfigItem;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.topic.TopicDetailOutput;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.topic.TopicListItem;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.topic.TopicOutput;
+import 
org.apache.rocketmq.studio.ops.ai.tool.contract.topic.TopicQueueStatsItem;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.topic.TopicRouteItem;
+import org.apache.rocketmq.studio.ops.ai.tool.core.ToolDefinition;
+import 
org.apache.rocketmq.studio.ops.ai.tool.handler.dashboard.DashboardSummaryToolHandler;
+import 
org.apache.rocketmq.studio.ops.ai.tool.handler.nameserver.NameserverListToolHandler;
+import org.apache.rocketmq.studio.ops.ai.tool.contract.plan.ToolPlan;
 import org.junit.jupiter.api.Test;
 import org.springframework.core.io.DefaultResourceLoader;
 import tools.jackson.databind.json.JsonMapper;
 
 import java.time.LocalDateTime;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Golden output-contract sweep: one representative sample per catalog tool, 
validated through
+ * the same {@link ToolSchemaValidator} pipeline the runtime filter chain 
uses. Keeps the yaml
+ * output schemas and the Java contract records in a 40/40 bijection — a 
schema or record drift
+ * fails here with the exact offending tool and field.
+ */
 class ToolOutputSchemaContractTest {
 
+    private static final String INSTANCE = "instance-a";
+    private static final long TIMESTAMP = 1784246400000L;
+
     private final ToolCatalog catalog = new ToolCatalog(new 
DefaultResourceLoader());
     private final ToolSchemaValidator validator = new ToolSchemaValidator(
             catalog,
-            new 
com.fasterxml.jackson.databind.ObjectMapper().findAndRegisterModules(),
-            new JsonMapper());
+            new LegacyJackson2Config().jackson2ObjectMapper(),
+            JsonMapper.builder().build());
+
+    @Test
+    void validatesEveryToolOutputSampleTest() {
+        samples().forEach((tool, outputs) -> outputs.forEach(output -> {
+            ToolDefinition definition = catalog.getDefinition(tool);
+            validator.validateOutput(definition, output);
+        }));
+    }
 
     @Test
-    void validatesTypedListOutputsAgainstCatalogSchemas() {
-        ClientConnectionVO client = ClientConnectionVO.builder()
-                .clientId("client-1")
-                .type(ClientType.Producer)
-                .groupOrTopic("orders")
-                .producerGroup("orders-producer")
-                .protocol(Protocol.Remoting)
-                .address("127.0.0.1:12000")
-                .language(ClientLanguage.Java)
-                .version("5.0.0")
-                .connectedAt(LocalDateTime.of(2026, 9, 7, 1, 0))
-                .partial(false)
-                .clusterName("cluster-a")
+    void coversEveryCatalogToolTest() {
+        Set<String> covered = new TreeSet<>(samples().keySet());
+        Set<String> declared = new TreeSet<>(catalog.list().stream()
+                .map(ToolDefinition::name)
+                .toList());
+        assertThat(covered).isEqualTo(declared);
+    }
+
+    private static Map<String, List<Object>> samples() {
+        Map<String, List<Object>> samples = new LinkedHashMap<>();
+
+        AclRuleItem aclRule = new AclRuleItem(
+                "1", "alice", "orders", "TOPIC", "LITERAL",
+                List.of("PUB"), "ALLOW", INSTANCE, "v2", 
"2026-08-22T08:00:00");
+        AclRuleVO aclRuleVO = AclRuleVO.builder()
+                .id(1L)
+                .principal("alice")
+                .resource("orders")
+                .resourceType("TOPIC")
+                .resourcePattern("LITERAL")
+                .actions(List.of("PUB"))
+                .decision("ALLOW")
+                .scope(INSTANCE)
+                .aclVersion("v2")
+                .gmtCreate(LocalDateTime.of(2026, 8, 22, 8, 0))
                 .build();
+        samples.put("rmq.acl.get", List.of(aclRule));
+        samples.put("rmq.acl.list", List.of(new PageOutput<>(1, 20, 1L, 
List.of(aclRule))));
+        samples.put("rmq.acl.create", List.of(planned(), executed(aclRuleVO)));
+        samples.put("rmq.acl.update", List.of(planned(), executed(aclRuleVO)));
+        samples.put("rmq.acl.delete", List.of(planned(), executedVoid()));
+
+        AclUserItem user = new AclUserItem("1", "alice", true, 
List.of(INSTANCE));
+        samples.put("rmq.user.get", List.of(user));
+        samples.put("rmq.user.list", List.of(new ListOutput<>(List.of(user))));
+        samples.put("rmq.user.create", List.of(planned(), executed(user)));
+        samples.put("rmq.user.delete", List.of(planned(), executedVoid()));
+
+        samples.put("rmq.alert.rule.list", List.of(new ListOutput<>(List.of(
+                new AlertRuleListItem(1L, "consumer-lag", 
"consumer.lag.total", ">",
+                        1000.0, "count", "5m", List.of("dingtalk"), true, "lag 
alert")))));
+
+        samples.put("rmq.audit.list", List.of(new PageOutput<>(1, 20, 1L, 
List.of(
+                new AuditItem(1L, "2026-08-22T08:00:00", "admin", 
"CREATE_TOPIC", "TOPIC",
+                        "orders", INSTANCE, "{}", "SUCCESS", null)))));
+
+        samples.put("rmq.dashboard.summary", List.of(new 
DashboardSummaryToolHandler.Output(
+                List.of(new DashboardSummaryToolHandler.Cluster(
+                        INSTANCE, INSTANCE, "V4_DIRECT", "healthy",
+                        1, 0, 2, 3, 10L, 9L, "V5_5_0", List.of(1, 2))),
+                new DashboardSummaryToolHandler.Stats(
+                        1, 1, 1, 0, 1, 2, 3, 100L, 5L, 10L, 9L))));
+
+        samples.put("rmq.instance.capabilities", List.of(
+                new InstanceCapabilitiesOutput(INSTANCE, 
List.of("TOPIC_MANAGEMENT"))));
+        samples.put("rmq.instance.metrics", List.of(MetricDataVO.builder()
+                .resultType("matrix")
+                .series(List.of(MetricDataVO.MetricSeriesVO.builder()
+                        .labels(Map.of("cluster", "rmq-a"))
+                        .values(List.of(MetricDataVO.MetricSampleVO.builder()
+                                .timestamp(1784246400.0)
+                                .value("1.5")
+                                .build()))
+                        .build()))
+                .warnings(List.of())
+                .build()));
+
         BrokerVO broker = BrokerVO.builder()
                 .name("broker-a")
                 .addr("127.0.0.1:10911")
-                .version("5.0.0")
+                .version("V5_5_0")
                 .status(BrokerStatus.running)
                 .diskUsage(0.25)
                 .tpsIn(10)
                 .tpsOut(9)
+                .putMessagesToday(100)
+                .putMessagesYesterday(90)
+                .getMessagesToday(80)
+                .getMessagesYesterday(70)
                 .runtimeStatsAvailable(true)
                 .build();
-        ProxyVO proxy = ProxyVO.builder()
+        samples.put("rmq.broker.list", List.of(new 
ListOutput<>(List.of(broker))));
+        samples.put("rmq.broker.describe", List.of(new BrokerDescribeOutput(
+                "broker-a", "127.0.0.1:10911", "V5_5_0", BrokerStatus.running,
+                0.25, 10L, 9L, 100L, 90L, 80L, 70L, true)));
+        samples.put("rmq.broker.config", List.of(new BrokerConfigOutput(
+                "rmq-a", true, false, 1, 1,
+                List.of("maxMessageSize"),
+                List.of(new BrokerConfigOutput.BrokerStatus(
+                        "broker-a", "127.0.0.1:10911", true, null)),
+                List.of(new BrokerConfigOutput.ConfigDifference(
+                        "maxMessageSize", "maxMessageSize",
+                        List.of(new BrokerConfigOutput.ConfigValue(
+                                "broker-a", "127.0.0.1:10911", true, 
"4194304")))))));
+
+        samples.put("rmq.cluster.list", List.of(new ListOutput<>(List.of(
+                new ClusterListItem("rmq-a", "127.0.0.1:10911", "broker-a", 
0L, "V5_5_0")))));
+
+        samples.put("rmq.nameserver.list", List.of(new ListOutput<>(List.of(
+                new NameserverListToolHandler.Item(
+                        "127.0.0.1:9876", "127.0.0.1:9876", "127.0.0.1:9876",
+                        null, null, "UNKNOWN", null)))));
+        samples.put("rmq.nameserver.config", List.of(new ListOutput<>(List.of(
+                new NameserverConfigItem("127.0.0.1:9876", 
Map.of("orderMessageEnable", "false"))))));
+
+        samples.put("rmq.proxy.list", List.of(new 
ListOutput<>(List.of(ProxyVO.builder()
                 .addr("127.0.0.1:8081")
                 .status(ClusterStatus.healthy)
                 .connections(3)
                 .grpcPort(8081)
                 .remotingPort(8080)
+                .build()))));
+        samples.put("rmq.proxy.config", List.of(new ListOutput<>(List.of(
+                new ProxyConfigItem("127.0.0.1:8081", "healthy", 3, 8081, 8080,
+                        true, true, "V5_5_0")))));
+
+        TopicListItem topicItem = new TopicListItem(
+                "orders", INSTANCE, TopicType.NORMAL, 8, 8, TopicPerm.RW, 
100L, 1.5, 2);
+        samples.put("rmq.topic.list", List.of(new 
ListOutput<>(List.of(topicItem))));
+        TopicRouteItem route = new TopicRouteItem(
+                "broker-a", "127.0.0.1:10911", "127.0.0.1:10911",
+                Map.of(0L, "127.0.0.1:10911"), List.of(0L), 1, 8, 8,
+                "RW", 6, true, true, 0);
+        samples.put("rmq.topic.route", List.of(new 
ListOutput<>(List.of(route))));
+        samples.put("rmq.topic.detail", List.of(new TopicDetailOutput(
+                INSTANCE, "orders", INSTANCE, TopicType.NORMAL, 8, 8, 
TopicPerm.RW,
+                100L, 1.5, 2, "order topic",
+                List.of(new TopicDetailOutput.ConsumerGroup(
+                        "cg-orders", "CLUSTERING", "CLUSTERING", 1.5, 50L, 
true)),
+                List.of(new TopicDetailOutput.Route(
+                        "broker-a", "127.0.0.1:10911", "127.0.0.1:10911",
+                        Map.of(0L, "127.0.0.1:10911"), List.of(0L), 1, 8, 8,
+                        "RW", 6, true, true, 0)),
+                List.of(new TopicQueueStatsItem("broker-a", 0, 0L, 100L, 
TIMESTAMP)))));
+        samples.put("rmq.topic.update", List.of(planned(), executed(new 
TopicOutput(
+                "orders", INSTANCE, "NORMAL", 8, 8, "RW", "order topic"))));
+        samples.put("rmq.topic.delete", List.of(planned(), executedVoid()));
+
+        GroupListItem groupItem = new GroupListItem(
+                "cg-orders", INSTANCE, SubscriptionMode.Push, 
ConsumeType.CLUSTERING,
+                16, 2, 100L, List.of("orders"));
+        samples.put("rmq.group.list", List.of(new 
ListOutput<>(List.of(groupItem))));
+        samples.put("rmq.group.detail", List.of(new GroupDetailOutput(
+                INSTANCE, "cg-orders", SubscriptionMode.Push, 
ConsumeType.CLUSTERING,
+                2, 100L, List.of("orders"), "TAG", "Concurrently", 16, 0,
+                List.of(new GroupDetailOutput.Subscription(
+                        "orders", "*", "TAG", "STANDARD", "CONSISTENT")),
+                List.of(new GroupDetailOutput.Instance(
+                        "client-1", "gRPC", "127.0.0.1:50000", 
List.of("orders"),
+                        "2026-08-22T09:30:00", Map.of("orders", 10L))),
+                new GroupDetailOutput.Health("HEALTHY", List.of()),
+                List.of(groupItem),
+                new GroupDetailOutput.Progress(100L, List.of(
+                        new GroupDetailOutput.QueueProgress("broker-a", 0, 
120L, 90L, 30L))),
+                new GroupDetailOutput.Clients(1, List.of(
+                        new GroupDetailOutput.Client(
+                                "client-1", "gRPC", "127.0.0.1:50000", "JAVA", 
"5.0.7",
+                                true, List.of("orders"), "2026-08-22T09:30:00",
+                                Map.of("orders", 10L)))))));
+        samples.put("rmq.group.update", List.of(planned(), 
executed(groupItem)));
+        samples.put("rmq.group.delete", List.of(planned(), executedVoid()));
+        samples.put("rmq.group.reset_offset", List.of(
+                planned(),
+                executed(new ResetOffsetOutput("cg-orders", "orders", 
TIMESTAMP, true))));
+
+        MessageItem message = new MessageItem(
+                "MSG-1", "orders", "tagA", "keyA", TIMESTAMP,
+                "127.0.0.1:10911", "127.0.0.1:50000", "aGVsbG8=", "BASE64", 
false, 5);
+        samples.put("rmq.message.query", List.of(new 
ListOutput<>(List.of(message))));
+        samples.put("rmq.message.query_by_topic", List.of(new 
ListOutput<>(List.of(message))));
+        samples.put("rmq.message.query_by_offset", List.of(new 
ListOutput<>(List.of(message))));
+        samples.put("rmq.message.query_dlq", List.of(
+                MessageQueryDlqOutput.ofGroups(INSTANCE, 1, 20, 1L, List.of(
+                        new MessageQueryDlqOutput.DlqGroupItem(
+                                "cg-orders", "%DLQ%cg-orders", 5, 16, 
"CONSUMED",
+                                true, "2026-08-22T09:30:00"))),
+                MessageQueryDlqOutput.ofMessages(INSTANCE, "cg-orders", 1, 20, 
1L, List.of(
+                        new MessageQueryDlqOutput.DlqMessageItem(
+                                "MSG-9", "%DLQ%cg-orders", 0, 12L, TIMESTAMP, 
"keyA", "hello")))));
+        samples.put("rmq.message.trace", List.of(new MessageTraceOutput(
+                "MSG-1",
+                List.of(new MessageTraceOutput.Node(
+                        "SEND", TIMESTAMP, "SUCCESS", 5L, "message sent")),
+                List.of(new MessageTraceOutput.ConsumerStatus(
+                        "cg-orders", "CONSUMED", TIMESTAMP, 0)))));
+        samples.put("rmq.message.send", List.of(planned(), executed(
+                new MessageSendOutput("MSG-1", "OFF-1", TIMESTAMP))));
+        samples.put("rmq.message.redelivery", List.of(planned(), executed(
+                new MessageRedeliveryOutput("MSG-1", "MSG-2", 
"%RETRY%cg-orders"))));
+        samples.put("rmq.message.redelivery_dlq", List.of(planned(), executed(
+                new MessageRedeliveryDlqOutput(5, 5, 0, "ALL_RESENT", false, 
0))));
+
+        return samples;
+    }
+
+    private static ToolPlan plan() {
+        return ToolPlan.builder("preview summary")
+                .impact("one impact")
                 .build();
+    }
+
+    private static <R> MutationOutput<R> planned() {
+        return new MutationOutput<>(
+                MutationOutput.Status.PLANNED, INSTANCE, plan(), 
"confirm-token-1", null);
+    }
+
+    private static <R> MutationOutput<R> executed(R result) {
+        return new MutationOutput<>(
+                MutationOutput.Status.EXECUTED, INSTANCE, plan(), null, 
result);
+    }
 
-        validator.validateOutput(
-                catalog.getDefinition("rmq.client.list"),
-                new ClusterListOutput<>("cluster-a", List.of(client)));
-        validator.validateOutput(
-                catalog.getDefinition("rmq.broker.list"),
-                new ClusterListOutput<>("cluster-a", List.of(broker)));
-        validator.validateOutput(
-                catalog.getDefinition("rmq.proxy.list"),
-                new ClusterListOutput<>("cluster-a", List.of(proxy)));
+    private static MutationOutput<Void> executedVoid() {
+        return executed(null);
     }
 
 }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolTokenConfigurationTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolTokenConfigurationTest.java
index 91381212c..e1508b4cb 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolTokenConfigurationTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolTokenConfigurationTest.java
@@ -86,13 +86,13 @@ class ToolTokenConfigurationTest {
         runner.run(context -> {
             assertThat(context).hasNotFailed();
             ToolCatalog catalog = context.getBean(ToolCatalog.class);
-            ToolExecutionContext request = executionContext(catalog, 
"rmq.topic.create", Map.of("topic", "orders"));
+            ToolExecutionContext request = executionContext(catalog, 
"rmq.topic.update", Map.of("topic", "orders"));
             ToolTokenService tokens = context.getBean(ToolTokenService.class);
             assertThatThrownBy(() -> 
tokens.issue(request)).isInstanceOfSatisfying(ToolExecutionException.class,
                     failure -> 
assertThat(failure.getErrorCode()).isEqualTo("UNAVAILABLE"));
             assertThatThrownBy(() -> 
tokens.verify(request)).isInstanceOfSatisfying(ToolExecutionException.class,
                     failure -> 
assertThat(failure.getErrorCode()).isEqualTo("UNAVAILABLE"));
-            ToolExecutionContext read = executionContext(catalog, 
"rmq.topic.list", Map.of("cluster", "dev"));
+            ToolExecutionContext read = executionContext(catalog, 
"rmq.topic.list", Map.of("instanceId", "dev"));
             assertThat(context.getBean(ToolFilterChain.class).execute(
                     new ToolInvocation(read, 
previewHandler("rmq.topic.list")))).isEqualTo(read.input());
         });
@@ -105,14 +105,14 @@ class ToolTokenConfigurationTest {
                     assertThat(context).hasNotFailed();
                     ToolFilterChain chain = 
context.getBean(ToolFilterChain.class);
                     ToolCatalog catalog = context.getBean(ToolCatalog.class);
-                    MutationToolHandler<Map<String, Object>, Object> handler = 
previewHandler("rmq.topic.create");
-                    ToolExecutionContext preview = executionContext(catalog, 
"rmq.topic.create", Map.of(
-                            "cluster", "dev", "topic", "orders", "dry_run", 
true));
+                    MutationToolHandler<Map<String, Object>, Object> handler = 
previewHandler("rmq.topic.update");
+                    ToolExecutionContext preview = executionContext(catalog, 
"rmq.topic.update", Map.of(
+                            "instanceId", "dev", "topic", "orders", "dry_run", 
true));
                     MutationOutput<?> plan = (MutationOutput<?>) 
chain.execute(new ToolInvocation(preview, handler));
                     Map<?, ?> output = new ObjectMapper().convertValue(plan, 
Map.class);
                     
assertThat(output.get("confirm_token")).isInstanceOf(String.class);
-                    ToolExecutionContext apply = executionContext(catalog, 
"rmq.topic.create", Map.of(
-                            "cluster", "dev", "topic", "orders", 
"confirm_token", output.get("confirm_token")));
+                    ToolExecutionContext apply = executionContext(catalog, 
"rmq.topic.update", Map.of(
+                            "instanceId", "dev", "topic", "orders", 
"confirm_token", output.get("confirm_token")));
                     AtomicBoolean executed = new AtomicBoolean();
                     MutationOutput<?> result = (MutationOutput<?>) 
chain.execute(new ToolInvocation(apply, executeHandler(executed)));
                     assertThat(executed).isTrue();
@@ -150,7 +150,7 @@ class ToolTokenConfigurationTest {
         return new MutationToolHandler<>((Class<Map<String, Object>>) 
(Class<?>) Map.class) {
             @Override
             public String name() {
-                return "rmq.topic.create";
+                return "rmq.topic.update";
             }
 
             @Override
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolTokenServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolTokenServiceTest.java
index 3cca17af3..9a01fa21f 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolTokenServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/service/ToolTokenServiceTest.java
@@ -42,8 +42,8 @@ class ToolTokenServiceTest {
 
     private static final byte[] SECRET = 
"0123456789abcdef0123456789abcdef".getBytes(StandardCharsets.UTF_8);
     private static final Clock CLOCK = 
Clock.fixed(Instant.parse("2026-09-11T02:00:00Z"), ZoneOffset.UTC);
-    private static final Map<String, Object> INPUT = Map.of("cluster", 
"instance-dev", "topic", "orders", "writeQueues", 8);
-    private static final String TOKEN = 
"djEuMTc4OTA5MjYwMC5gRwUvzs+4BUqDX0svsV1C/jUjLzfv/GDPZpjcG9bvBg==";
+    private static final Map<String, Object> INPUT = Map.of("instanceId", 
"instance-dev", "topicName", "orders", "writeQueues", 8);
+    private static final String TOKEN = 
"djEuMTc4OTA5MjYwMC5BjvIrDnoYt1d8qKs67Cu8qBBPauIQJ6uJg6OUDD5IIg==";
     private final ToolCatalog catalog = new ToolCatalog(new 
DefaultResourceLoader());
     private final ToolTokenService tokens = new ToolTokenService(new 
ObjectMapper(), CLOCK, SECRET);
 
@@ -56,8 +56,8 @@ class ToolTokenServiceTest {
 
         Map<String, Object> apply = new LinkedHashMap<>();
         apply.put("writeQueues", 8);
-        apply.put("topic", "orders");
-        apply.put("cluster", "instance-dev");
+        apply.put("topicName", "orders");
+        apply.put("instanceId", "instance-dev");
         apply.put("dry_run", false);
         apply.put("break_glass", true);
         apply.put("reason", "reviewed");
@@ -72,9 +72,9 @@ class ToolTokenServiceTest {
     void bindsToolCallerInstanceAndBusinessInput() {
         List<ToolExecutionContext> changedRequests = List.of(
                 ToolExecutionContext.of("instance-dev", 
catalog.getDefinition("rmq.topic.delete"), INPUT, "alice"),
-                ToolExecutionContext.of("instance-dev", 
catalog.getDefinition("rmq.topic.create"), INPUT, "bob"),
-                ToolExecutionContext.of("other-instance", 
catalog.getDefinition("rmq.topic.create"), INPUT, "alice"),
-                context(Map.of("cluster", "instance-dev", "topic", "orders", 
"writeQueues", 16)));
+                ToolExecutionContext.of("instance-dev", 
catalog.getDefinition("rmq.topic.update"), INPUT, "bob"),
+                ToolExecutionContext.of("other-instance", 
catalog.getDefinition("rmq.topic.update"), INPUT, "alice"),
+                context(Map.of("instanceId", "instance-dev", "topicName", 
"orders", "writeQueues", 16)));
         changedRequests.forEach(request -> assertInvalid(tokens, 
withToken(request, TOKEN)));
     }
 
@@ -107,22 +107,22 @@ class ToolTokenServiceTest {
 
     @Test
     void acceptsRawSignatureContainingDotsAndNonUtf8Bytes() {
-        ToolExecutionContext request = context(Map.of("cluster", 
"instance-dev", "topic", "orders-6", "writeQueues", 8));
+        ToolExecutionContext request = context(Map.of("instanceId", 
"instance-dev", "topicName", "orders-20", "writeQueues", 8));
         String token = tokens.issue(request);
-        
assertThat(token).isEqualTo("djEuMTc4OTA5MjYwMC7zC5yHz0AaCGORzV7ZLGn0TGM56j74zi5atWjYC9VlMA==");
+        
assertThat(token).isEqualTo("djEuMTc4OTA5MjYwMC79LrkcIaZYjpm1r3pi9tpCPstorTcGDQuCw2FQFvMKwQ==");
         byte[] content = Base64.getDecoder().decode(token);
         assertThat(Arrays.copyOfRange(content, content.length - 32, 
content.length)).contains((byte) '.', (byte) 0xf3);
         assertThatCode(() -> tokens.verify(withToken(request, 
token))).doesNotThrowAnyException();
     }
 
     private ToolExecutionContext context(Map<String, Object> input) {
-        return ToolExecutionContext.of("instance-dev", 
catalog.getDefinition("rmq.topic.create"), input, "alice");
+        return ToolExecutionContext.of("instance-dev", 
catalog.getDefinition("rmq.topic.update"), input, "alice");
     }
 
     private static ToolExecutionContext withToken(ToolExecutionContext 
context, String token) {
         Map<String, Object> input = new LinkedHashMap<>(context.input());
         input.put("confirm_token", token);
-        return ToolExecutionContext.of(context.cluster(), 
context.definition(), input, context.principal());
+        return ToolExecutionContext.of(context.instanceId(), 
context.definition(), input, context.principal());
     }
 
     private static void assertInvalid(ToolTokenService service, 
ToolExecutionContext context) {
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceControllerTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceControllerTest.java
index 00cc48f47..2bb8f5d8c 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceControllerTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertSilenceControllerTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.rocketmq.studio.ops.alert;
 
+import org.apache.rocketmq.studio.WebMvcAuthTestSupport;
 import org.apache.rocketmq.studio.common.domain.PageResult;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -40,7 +41,7 @@ import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 
 @WebMvcTest(AlertSilenceController.class)
 @AutoConfigureMockMvc(addFilters = false)
-class AlertSilenceControllerTest {
+class AlertSilenceControllerTest extends WebMvcAuthTestSupport {
 
     @Autowired
     private MockMvc mockMvc;
diff --git a/web/src/api/ai.test.ts b/web/src/api/ai.test.ts
index 89b71029d..65c4024fa 100644
--- a/web/src/api/ai.test.ts
+++ b/web/src/api/ai.test.ts
@@ -306,12 +306,16 @@ describe('AI API', () => {
   });
 
   describe('executeTool', () => {
-    it('posts structured input and returns structured output', async () => {
-      const input = { cluster: 'cluster-a', topic: 'orders' };
+    it('posts structured input with the instanceId query param and returns 
structured output', async () => {
+      const input = { instanceId: 'cluster-a', topicName: 'orders' };
       const output = { items: [{ name: 'orders' }], total: 1 };
-      mock.onPost('/ai/tools/rmq.topic.list/execute', input).reply(200, { 
data: output });
+      mock
+        .onPost('/ai/tools/rmq.topic.list/execute', input, {
+          params: { instanceId: 'cluster-a' },
+        })
+        .reply(200, { data: output });
 
-      await expect(executeTool('rmq.topic.list', 
input)).resolves.toEqual(output);
+      await expect(executeTool('rmq.topic.list', input, 
'cluster-a')).resolves.toEqual(output);
     });
   });
 });
diff --git a/web/src/api/ai.ts b/web/src/api/ai.ts
index 564af6db0..c05761288 100644
--- a/web/src/api/ai.ts
+++ b/web/src/api/ai.ts
@@ -258,10 +258,15 @@ export async function listTools(cluster?: string) {
   return res.data.data;
 }
 
-export async function executeTool(name: string, input: Record<string, 
unknown>) {
+export async function executeTool(
+  name: string,
+  input: Record<string, unknown>,
+  instanceId: string,
+) {
   const res = await client.post<{ data: unknown }>(
     `/ai/tools/${encodeURIComponent(name)}/execute`,
     input,
+    { params: { instanceId } },
   );
   return res.data.data;
 }
diff --git a/web/src/pages/ai/__tests__/AiPage.test.tsx 
b/web/src/pages/ai/__tests__/AiPage.test.tsx
index 19b0aa9da..ea72b1ff9 100644
--- a/web/src/pages/ai/__tests__/AiPage.test.tsx
+++ b/web/src/pages/ai/__tests__/AiPage.test.tsx
@@ -110,12 +110,12 @@ describe('AiPage tool runner', () => {
     ]);
     vi.mocked(listTools).mockResolvedValue([
       {
-        name: 'rmq.capabilities',
-        description: 'Describe cluster capabilities.',
+        name: 'rmq.instance.capabilities',
+        description: 'Describe instance capabilities.',
         parameters: {
           type: 'object',
-          required: ['cluster'],
-          properties: { cluster: { type: 'string' } },
+          required: ['instanceId'],
+          properties: { instanceId: { type: 'string' } },
         },
         riskLevel: 'L1',
         permission: 'cluster:read',
@@ -428,7 +428,7 @@ describe('AiPage tool runner', () => {
   it('loads the catalog, creates a schema template, and renders structured 
output', async () => {
     const user = userEvent.setup();
     vi.mocked(executeTool).mockResolvedValue({
-      cluster: 'cluster-a',
+      instanceId: 'cluster-a',
       capabilities: ['GRPC'],
     });
     renderPage();
@@ -438,19 +438,23 @@ describe('AiPage tool runner', () => {
     const dialog = await screen.findByRole('dialog', { name: 'AI 工具' });
     await waitFor(() => expect(listTools).toHaveBeenCalledWith('cluster-a'));
     expect(within(dialog).getByText('Cluster A')).toBeInTheDocument();
-    expect(within(dialog).getByText('rmq.capabilities')).toBeInTheDocument();
+    
expect(within(dialog).getByText('rmq.instance.capabilities')).toBeInTheDocument();
     expect(within(dialog).getByText('L1')).toBeInTheDocument();
     expect(within(dialog).getByText('cluster:read')).toBeInTheDocument();
 
     const input = within(dialog).getByRole('textbox', { name: '工具参数 JSON' });
-    expect(input).toHaveValue('{\n  "cluster": "cluster-a"\n}');
-    fireEvent.change(input, { target: { value: '{"cluster":"cluster-a"}' } });
+    expect(input).toHaveValue('{\n  "instanceId": "cluster-a"\n}');
+    fireEvent.change(input, { target: { value: '{"instanceId":"cluster-a"}' } 
});
     await user.click(within(dialog).getByRole('button', { name: /执\s*行/ }));
 
     await waitFor(() => {
-      expect(executeTool).toHaveBeenCalledWith('rmq.capabilities', {
-        cluster: 'cluster-a',
-      });
+      expect(executeTool).toHaveBeenCalledWith(
+        'rmq.instance.capabilities',
+        {
+          instanceId: 'cluster-a',
+        },
+        'cluster-a',
+      );
     });
     expect(await 
within(dialog).findByTestId('tool-result')).toHaveTextContent('"capabilities": 
[');
     
expect(within(dialog).getByTestId('tool-result')).toHaveTextContent('"GRPC"');
@@ -499,8 +503,8 @@ describe('AiPage tool runner', () => {
         description: `Tool for ${cluster}`,
         parameters: {
           type: 'object',
-          required: ['cluster'],
-          properties: { cluster: { type: 'string' } },
+          required: ['instanceId'],
+          properties: { instanceId: { type: 'string' } },
         },
       },
     ]);
@@ -519,7 +523,7 @@ describe('AiPage tool runner', () => {
     await waitFor(() => expect(listTools).toHaveBeenCalledWith('cluster-b'));
     expect(within(dialog).getByText('rmq.tool.cluster-b')).toBeInTheDocument();
     expect(within(dialog).getByRole('textbox', { name: '工具参数 JSON' 
})).toHaveValue(
-      '{\n  "cluster": "cluster-b"\n}',
+      '{\n  "instanceId": "cluster-b"\n}',
     );
   });
 
diff --git a/web/src/pages/ai/index.tsx b/web/src/pages/ai/index.tsx
index cbc6a89f8..e5fadb317 100644
--- a/web/src/pages/ai/index.tsx
+++ b/web/src/pages/ai/index.tsx
@@ -170,7 +170,7 @@ const buildToolInputTemplate = (tool: McpTool, cluster?: 
string): string => {
   const input = Object.fromEntries(
     required.map((field) => [
       field,
-      field === 'cluster' && cluster ? cluster : 
defaultSchemaValue(properties[field]),
+      field === 'instanceId' && cluster ? cluster : 
defaultSchemaValue(properties[field]),
     ]),
   );
   return JSON.stringify(input, null, 2);
@@ -885,14 +885,14 @@ const AiPage = () => {
     setToolExecuting(true);
     setToolResult(undefined);
     try {
-      setToolResult(await executeTool(selectedToolName, parsedInput));
+      setToolResult(await executeTool(selectedToolName, parsedInput, 
selectedClusterId));
       message.success('工具执行成功');
     } catch (error) {
       message.error(error instanceof Error ? error.message : '工具执行失败');
     } finally {
       setToolExecuting(false);
     }
-  }, [selectedToolName, toolExecuting, toolInput]);
+  }, [selectedClusterId, selectedToolName, toolExecuting, toolInput]);
 
   const selectedTool = tools.find((tool) => tool.name === selectedToolName);
 

Reply via email to