refactor(skill-mcp): expose tool display names via ResolvedSkill instead of Map view

This commit is contained in:
matevip 2026-05-10 23:46:37 +08:00
parent 18148aa724
commit 9cac7a31d1
3 changed files with 51 additions and 55 deletions

View File

@ -1,8 +1,6 @@
package vip.mate.skill.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
@ -59,7 +57,6 @@ public class SkillController {
private final AgentBindingService agentBindingService;
private final vip.mate.skill.mcp.McpSkillBridge mcpSkillBridge;
private final vip.mate.skill.acp.AcpSkillBridge acpSkillBridge;
private final ObjectMapper objectMapper;
@Operation(summary = "获取技能分页列表RFC-042 §2.1")
@GetMapping
@ -395,34 +392,8 @@ public class SkillController {
@Operation(summary = "获取所有技能的运行时解析状态(管理页面使用)")
@GetMapping("/runtime/status")
public R<List<Map<String, Object>>> getRuntimeStatus() {
List<ResolvedSkill> skills = skillRuntimeService.resolveAllSkillsStatus();
return R.ok(skills.stream().map(this::toRuntimeStatusView).toList());
}
/**
* Perform a display transformation on `effectiveAllowedTools` (returned to the frontend),
* and store the transformed result in `effectiveAllowedToolsDisplay`.
* In addition to displaying the tool name in the format `mcp_<serverId>_<slug>_<hash6>`,
* also display the original MCP tool name in parentheses immediately following it.
*
* @param skill
* @return
*/
private Map<String, Object> toRuntimeStatusView(ResolvedSkill skill) {
Map<String, Object> view = objectMapper.convertValue(
skill,
new TypeReference<Map<String, Object>>() {}
);
if ("mcp".equalsIgnoreCase(skill.getSource())) {
List<String> displayTools = skill.getEffectiveAllowedTools().stream()
.map(mcpSkillBridge::decorateToolNameForDisplay)
.toList();
view.put("effectiveAllowedToolsDisplay", displayTools);
}
return view;
public R<List<ResolvedSkill>> getRuntimeStatus() {
return R.ok(skillRuntimeService.resolveAllSkillsStatus());
}
@Operation(summary = "刷新 active skills 缓存resync=true 时同步内置技能到 workspace")

View File

@ -121,26 +121,6 @@ public class McpSkillBridge {
}
}
/**
* Add a "display-only" method to reverse-lookup a prefixed name back to its raw name.
*
* @param prefixedName
* @return
*/
public String decorateToolNameForDisplay(String prefixedName) {
McpToolNameResolver.ParsedRef ref = McpToolNameResolver.parse(prefixedName);
if (ref == null) return prefixedName;
McpServerEntity server = mcpServerService.getById(ref.serverId());
for (String raw : readToolRawNames(server)) {
String rebuilt = McpToolNameResolver.prefixedName(ref.serverId(), raw);
if (rebuilt.equals(prefixedName)) {
return prefixedName + " (" + raw + ")";
}
}
return prefixedName;
}
private List<McpServerEntity> listEnabledServers() {
try {
return mcpServerService.listEnabled();
@ -166,12 +146,18 @@ public class McpSkillBridge {
s.setTags("mcp");
s.setSecurityScanStatus("PASSED"); // MCP servers don't go through SkillSecurityService
s.setConfigJson(buildConfigJson(server));
s.setManifestJson(serializeManifest(buildManifest(server)));
s.setManifestJson(serializeManifest(buildManifestFrom(server, readToolRawNames(server))));
return s;
}
private ResolvedSkill serverToResolved(McpServerEntity server) {
SkillManifest manifest = buildManifest(server);
List<String> rawNames = readToolRawNames(server);
Map<String, String> toolDisplayNames = new LinkedHashMap<>();
for (String raw : rawNames) {
String prefixed = McpToolNameResolver.prefixedName(server.getId(), raw);
toolDisplayNames.put(prefixed, prefixed + " (" + raw + ")");
}
SkillManifest manifest = buildManifestFrom(server, rawNames);
boolean connected = "connected".equalsIgnoreCase(nullSafe(server.getLastStatus()));
boolean errored = "error".equalsIgnoreCase(nullSafe(server.getLastStatus()))
|| (server.getLastError() != null && !server.getLastError().isBlank());
@ -212,6 +198,7 @@ public class McpSkillBridge {
.manifest(manifest)
.featureStatuses(featureStatuses)
.activeFeatures(active)
.toolDisplayNames(toolDisplayNames)
.build();
}
@ -234,8 +221,7 @@ public class McpSkillBridge {
* don't appear in any agent's callbacks at chat time, and the LLM
* would see no MCP tools even though the bindings were saved.
*/
private SkillManifest buildManifest(McpServerEntity server) {
List<String> rawNames = readToolRawNames(server);
private SkillManifest buildManifestFrom(McpServerEntity server, List<String> rawNames) {
List<String> toolNames = new ArrayList<>(rawNames.size());
for (String raw : rawNames) {
toolNames.add(McpToolNameResolver.prefixedName(server.getId(), raw));

View File

@ -130,6 +130,25 @@ public class ResolvedSkill {
@Builder.Default
private Set<String> activeFeatures = Set.of();
/**
* Per-tool display-name decoration table, keyed by the prefixed callback
* name and valued by the human-readable form (e.g.
* {@code "mcp_4_fs_a1b2c3"} {@code "mcp_4_fs_a1b2c3 (read_file)"}).
*
* <p>Populated by skill source providers that have a recoverable raw
* name (currently MCP-bridged skills); other sources leave it empty,
* in which case {@link #getEffectiveAllowedToolsDisplay()} falls
* through to the prefixed names unchanged.
*
* <p>Held internally rather than serialized: the wire shape exposes
* the decorated set via the derived getter, which keeps the
* source-of-truth (the feature filter in
* {@link #getEffectiveAllowedTools()}) in one place.
*/
@JsonIgnore
@Builder.Default
private Map<String, String> toolDisplayNames = Map.of();
/** RFC-090 §14.1 — replacement filter for {@code dependencyReady}. */
public boolean hasAnyActiveFeature() {
return activeFeatures != null && !activeFeatures.isEmpty();
@ -200,6 +219,26 @@ public class ResolvedSkill {
return out;
}
/**
* Display-friendly companion to {@link #getEffectiveAllowedTools()}.
* Each prefixed callback name is replaced by its decorated form (e.g.
* {@code "mcp_4_fs_a1b2c3 (read_file)"}) when {@link #toolDisplayNames}
* carries an entry for it; names without a decoration entry are kept
* verbatim. Feature-filter semantics match the prefixed getter, so a
* tool that is hidden by a SETUP_NEEDED feature stays hidden here too.
*/
public Set<String> getEffectiveAllowedToolsDisplay() {
Set<String> base = getEffectiveAllowedTools();
if (base.isEmpty() || toolDisplayNames == null || toolDisplayNames.isEmpty()) {
return base;
}
Set<String> out = new LinkedHashSet<>(base.size());
for (String name : base) {
out.add(toolDisplayNames.getOrDefault(name, name));
}
return out;
}
// ==================== 综合状态 ====================
/**