mcp - McpSkill Client 与 Server 设计参考
2026年1月26日 上午9:13:39
通过 MCP (Model Context Protocol) 协议,可以实现 AI 技能(Skill)分布式(或跨进程)部署,进而实现 Solon AI Remote Skills。McpSkillClient 作为代理实现 Skill 接口,而 McpSkillServer 负责将本地技能生命周期映射为远程调用端点。
设计提示
- hide 标记:我们在管理工具元数据中添加了
hide:1标记,确保这些系统级端点不会被泄露给 LLM,仅供客户端代理使用。 - 三态过滤机制:
getToolsName的设计允许服务端根据用户权限(如prompt.attr("user_role"))实现极其灵活的能力分发。
1、McpSkillClient (客户端代理)
McpSkillClient 是本地 Solon AI Skill 的“替身”,它通过网络协议与远程服务握手,并根据当前 Prompt 上下文实现感知。
import org.noear.snack4.Feature;
import org.noear.snack4.ONode;
import org.noear.solon.Utils;
import org.noear.solon.ai.chat.prompt.Prompt;
import org.noear.solon.ai.chat.skill.Skill;
import org.noear.solon.ai.chat.skill.SkillMetadata;
import org.noear.solon.ai.chat.tool.FunctionTool;
import org.noear.solon.lang.Preview;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* MCP 客户端技能代理
* <p>
* 职责:作为 MCP 客户端,将远程 MCP 服务的能力(工具、资源、指令)封装为本地 {@link Skill} 接口。
* 特点:支持跨进程的能力调用,并通过 {@link Prompt} 上下文实现远程准入检查与动态指令获取。
*
* @author noear
* @since 3.9.0
*/
@Preview("3.9.0")
public class McpSkillClient implements Skill {
/**
* MCP 客户端提供者,负责底层的通信协议(如 Stdio, SSE)
*/
protected final McpClientProvider clientProvider;
/**
* 缓存的技能元信息
*/
protected SkillMetadata metadata;
public McpSkillClient(McpClientProvider clientProvider) {
this.clientProvider = clientProvider;
// 从远程加载静态元信息(通过预定义的 Resource URI)
String metadataJson = clientProvider.readResourceAsText("skill://metadataMcp")
.getContent();
metadata = ONode.deserialize(metadataJson, SkillMetadata.class);
}
@Override
public String name() {
return metadata.getName();
}
@Override
public String description() {
return metadata.getDescription();
}
@Override
public SkillMetadata metadata() {
return metadata;
}
/**
* 跨进程准入检查:请求远程服务端判断当前 Prompt 环境是否允许激活该技能
*/
@Override
public boolean isSupported(Prompt prompt) {
String promptJson = ONode.serialize(prompt, Feature.Write_ClassName);
String result = clientProvider.callToolAsText("isSupportedMcp",
Utils.asMap("promptJson", promptJson))
.getContent();
return "true".equals(result);
}
@Override
public void onAttach(Prompt prompt) {
String promptJson = ONode.serialize(prompt, Feature.Write_ClassName);
clientProvider.callToolAsText("onAttachMcp",
Utils.asMap("promptJson", promptJson));
}
/**
* 动态指令获取:从远程服务端获取针对当前上下文优化后的 System Message 指令
*/
@Override
public String getInstruction(Prompt prompt) {
String promptJson = ONode.serialize(prompt, Feature.Write_ClassName);
return clientProvider.callToolAsText("getInstructionMcp",
Utils.asMap("promptJson", promptJson))
.getContent();
}
/**
* 获取远程导出的工具流
* <p>
* 过滤策略:自动剔除标记为 "hide" 的管理类工具(即元数据同步工具),仅保留业务工具
*/
protected Stream<FunctionTool> getToolsStream() {
return clientProvider.getTools().stream()
.filter(tool -> {
return tool.meta() == null || tool.meta().containsKey("hide") == false;
});
}
@Override
public Collection<FunctionTool> getTools(Prompt prompt) {
String promptJson = ONode.serialize(prompt, Feature.Write_ClassName);
String toolsNameJson = clientProvider.callToolAsText("getToolsMcp",
Utils.asMap("promptJson", promptJson))
.getContent();
List<String> toolsName = ONode.deserialize(toolsNameJson, List.class);
if(toolsName == null){
return getToolsStream().collect(Collectors.toList());
} else if(toolsName.isEmpty()) {
return Collections.EMPTY_LIST;
} else {
return getToolsStream().filter(tool -> toolsName.contains(tool.name()))
.collect(Collectors.toList());
}
}
}
2、McpSkillServer (服务端基类)
McpSkillServer 将 Skill 生命周期方法映射为标准的 MCP 端点(Tools & Resources),供客户端调用。
import org.noear.snack4.Feature;
import org.noear.snack4.ONode;
import org.noear.solon.ai.annotation.ResourceMapping;
import org.noear.solon.ai.annotation.ToolMapping;
import org.noear.solon.ai.chat.prompt.Prompt;
import org.noear.solon.ai.chat.prompt.PromptImpl;
import org.noear.solon.ai.chat.skill.Skill;
import org.noear.solon.ai.chat.tool.FunctionTool;
import org.noear.solon.lang.Preview;
import java.util.Collection;
import java.util.List;
/**
* MCP 服务端技能适配器
* <p>
* 职责:将本地定义的 {@link Skill} 逻辑通过 MCP 协议导出。
* 机制:利用注解将技能的生命周期方法(isSupported, getInstruction)映射为 MCP 的 Tool 或 Resource,
* 供远程 {@link org.noear.solon.ai.mcp.client.McpSkillClient} 发现并调用。
*
* @author noear
* @since 3.9.0
*/
@Preview("3.9.0")
public abstract class McpSkillServer implements Skill {
/**
* 导出技能元数据作为 MCP 资源
*/
@ResourceMapping(uri = "skill://metadataMcp", meta = "{hide:1}")
public String metadataMcp() {
return ONode.serialize(this.metadata());
}
/**
* 导出准入检查逻辑为 MCP 工具
* <p>
* 注意:此工具标记为 hide,通常由客户端代理调用,不对最终 LLM 暴露
*/
@ToolMapping(meta = "{hide:1}", description = "禁止 llm 使用")
public boolean isSupportedMcp(String promptJson) {
Prompt prompt = ONode.deserialize(promptJson, PromptImpl.class, Feature.Read_AutoType);
return this.isSupported(prompt);
}
/**
* 导出指令获取逻辑为 MCP 工具
*/
@ToolMapping(meta = "{hide:1}", description = "禁止 llm 使用")
public String getInstructionMcp(String promptJson) {
Prompt prompt = ONode.deserialize(promptJson, PromptImpl.class, Feature.Read_AutoType);
return this.getInstruction(prompt);
}
@ToolMapping(meta = "{hide:1}", description = "禁止 llm 使用")
public List<String> getToolsMcp(String promptJson) {
Prompt prompt = ONode.deserialize(promptJson, PromptImpl.class, Feature.Read_AutoType);
return this.getToolsName(prompt);
}
@ToolMapping(meta = "{hide:1}", description = "禁止 llm 使用")
public void onAttachMcp(String promptJson) {
Prompt prompt = ONode.deserialize(promptJson, PromptImpl.class, Feature.Read_AutoType);
this.onAttach(prompt);
}
@Override
public final Collection<FunctionTool> getTools(Prompt prompt) {
//不充许重载
return null;
}
public List<String> getToolsName(Prompt prompt){
// null 表示所有工具; 空表示无工具匹配;有表示要过滤名字
return null;
}
}