一、本章介绍
用户在使用 AI Agent 脚手架 时,通常会有扩展自定义 MCP 服务 的需求。比如在智能体客服、企业内部系统巡检等场景中,往往没有现成统一的 MCP 服务可直接使用,因此需要开发者根据自身业务实现本地 MCP 能力,并将其装配到整个智能体流程中。
同时,随着 MCP 接入方式的增加,系统中不仅需要支持 SSE、STDIO 等类型,还要兼容本地自定义实现的 MCP 服务。因此在装配阶段,需要引入相应的策略处理机制,根据不同的 MCP 类型选择对应的创建与加载方式,使整体扩展更加灵活,也便于后续维护。
二、流程设计
如图,增强 mcp 服务装配能力设计;

在 ChatModelNode 中装配 MCP 时,原有实现主要针对 SSE 和 STDIO 两种接入方式,因此通过
if...else判断即可完成不同类型的处理。但随着后续需要支持更多 MCP 装配方式,如果继续堆叠条件判断,会导致方法越来越臃肿,扩展和维护成本也会不断增加。因此,这部分需要将原本集中在一个方法中的逻辑拆分出来,通过 工厂模式 + 策略接口 的方式进行改造。由工厂负责根据 MCP 类型选择对应的装配策略,不同策略类分别处理各自的 MCP 创建逻辑,从而避免大量
if...else判断。同时,在设计上还需要区分 MCP 客户端 和 MCP 服务端 两部分。客户端侧主要负责 MCP 的策略装配和连接创建;服务端侧则面向用户自定义扩展,用于承载业务方自己实现的 MCP 服务能力。这样既能满足内置 MCP 的统一装配,也能支持用户根据实际业务场景扩展新的 MCP 服务。
三、工程实现
1. 工程结构
本次扩展新增 client 与 server 两部分能力。其中,client 负责不同类型 MCP 的装配与创建操作,通过统一的工厂和策略机制,对 SSE、STDIO 以及后续扩展的 MCP 类型进行管理;server 则作为 MCP 服务实现端,用于承载具体的 MCP 能力实现。用户可以根据业务需求自行开发 MCP 服务,并通过配置文件指定对应的 Bean 名称,最终完成自动装配。
同时,对 ChatModelNode 进行调整,不再直接处理 MCP 的创建逻辑,而是统一调用 MCP 工厂服务完成装配。这样可以将节点职责聚焦于模型构建,降低与具体 MCP 实现的耦合度。
此外,在 AiAgentConfigTableVO 的 MCP 配置项中新增 local 类型,用于支持本地 MCP 服务的装配。开发者只需在配置文件中声明对应的 Bean 名称,即可将自定义实现的 MCP 服务接入到智能体体系中,实现更加灵活的扩展能力。
2. 核心模块
2.1 结构设计
在 agent-service 模块下新增 mcp 模块,并进一步划分为 client 和 server 两个部分。
其中,server 作为 MCP 服务端,用于承载用户自定义实现的 MCP 服务能力;client 则作为 MCP 客户端,负责对接和装配不同类型的 MCP 服务,并通过策略模式完成扩展设计。
通过这种结构划分,可以将 MCP 服务实现与 MCP 接入装配解耦,既方便用户扩展自己的业务能力,也便于系统后续支持更多类型的 MCP 接入方式。
2.2 属性扩展(AiAgentConfigTableVO)
@Data
public static class ChatModel {
private String model;
private List<ToolMcp> toolMcpList;
@Data
public static class ToolMcp {
private SSEServerParameters sse;
private StdioServerParameters stdio;
private LocalParameters local;
@Data
public static class SSEServerParameters {
private String name;
private String baseUri;
private String sseEndpoint;
private Integer requestTimeout = 3000;
}
@Data
public static class StdioServerParameters {
private String name;
private Integer requestTimeout = 3000;
private ServerParameters serverParameters;
@Data
public static class ServerParameters {
private String command;
private List<String> args;
private Map<String, String> env;
}
}
@Data
public static class LocalParameters {
private String name;
}
}
}- 在 AiAgentConfigTableVO 的 ChatModel.ToolMcp 配置项中新增 local 属性,用于配置本地 MCP 服务对应的 Spring Bean 名称。当用户需要接入自定义实现的 MCP 服务时,只需在配置文件中填写对应的 Bean 名称,系统即可从 Spring 容器中获取实例并完成装配。这样既保留了对 SSE、STDIO 等远程 MCP 服务的支持,也为本地 MCP 服务扩展提供了统一的接入方式,进一步增强了 MCP 体系的灵活性和可扩展性。
2.3 装配策略
2.3.1 原本代码
@Override
protected AiAgentRegisterVO doApply(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
log.info("Ai Agent 装配操作 - ChatModelNode");
OpenAiApi openAiApi = dynamicContext.getOpenAiApi();
AiAgentConfigTableVO aiAgentConfigTableVO = requestParameter.getAiAgentConfigTableVO();
AiAgentConfigTableVO.Module.ChatModel chatModelConfig = aiAgentConfigTableVO.getModule().getChatModel();
List<McpSyncClient> mcpSyncClients = new ArrayList<>();
List<AiAgentConfigTableVO.Module.ChatModel.ToolMcp> toolMcpList = chatModelConfig.getToolMcpList();
if (null != toolMcpList) {
for (AiAgentConfigTableVO.Module.ChatModel.ToolMcp toolMcp : toolMcpList) {
mcpSyncClients.add(createMcpSyncClient(toolMcp));
}
}
ChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(OpenAiChatOptions.builder()
.model(chatModelConfig.getModel())
.toolCallbacks(createToolCallbacks(mcpSyncClients))
.build())
.build();
dynamicContext.setChatModel(chatModel);
return router(requestParameter, dynamicContext);
}
@Override
public StrategyHandler<ArmoryCommandEntity, DefaultArmoryFactory.DynamicContext, AiAgentRegisterVO> get(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
return agentNode;
}
private McpSyncClient createMcpSyncClient(AiAgentConfigTableVO.Module.ChatModel.ToolMcp toolMcp) throws Exception {
AiAgentConfigTableVO.Module.ChatModel.ToolMcp.SSEServerParameters sseConfig = toolMcp.getSse();
AiAgentConfigTableVO.Module.ChatModel.ToolMcp.StdioServerParameters stdioConfig = toolMcp.getStdio();
if (null != sseConfig) {
// http://appbuilder.baidu.com/v2/ai_search/mcp/sse?api_key=bce-v3/ALTAK-JFZXXLpfxhAutDQvJ32Ei/4492c1879b8c2f0df4612ef5b4a52df1c1fba9f7
String originalBaseUri = sseConfig.getBaseUri();
String baseUri = originalBaseUri;
String sseEndpoint = sseConfig.getSseEndpoint();
if (StringUtils.isBlank(sseEndpoint)) {
URL url = new URL(originalBaseUri);
String protocol = url.getProtocol();
String host = url.getHost();
int port = url.getPort();
String baseUrl = port == -1 ? protocol + "://" + host : protocol + "://" + host + ":" + port;
int index = originalBaseUri.indexOf(baseUrl);
if (index != -1) {
sseEndpoint = originalBaseUri.substring(index + baseUrl.length());
}
baseUri = baseUrl;
}
sseEndpoint = StringUtils.isBlank(sseEndpoint) ? "/sse" : sseEndpoint;
HttpClientSseClientTransport sseClientTransport = HttpClientSseClientTransport
.builder(baseUri)
.sseEndpoint(sseEndpoint)
.build();
McpSyncClient mcpSyncClient = McpClient
.sync(sseClientTransport)
.requestTimeout(Duration.ofMillis(sseConfig.getRequestTimeout())).build();
McpSchema.InitializeResult initialize = mcpSyncClient.initialize();
log.info("tool sse mcp initialize {}", initialize);
return mcpSyncClient;
}
if (null != stdioConfig) {
AiAgentConfigTableVO.Module.ChatModel.ToolMcp.StdioServerParameters.ServerParameters serverParameters = stdioConfig.getServerParameters();
ServerParameters stdioParams = ServerParameters.builder(serverParameters.getCommand())
.args(serverParameters.getArgs())
.env(serverParameters.getEnv())
.build();
McpSyncClient mcpSyncClient = McpClient.sync(new StdioClientTransport(stdioParams, new JacksonMcpJsonMapper(new ObjectMapper())))
.requestTimeout(Duration.ofSeconds(stdioConfig.getRequestTimeout())).build();
McpSchema.InitializeResult initialize = mcpSyncClient.initialize();
log.info("tool stdio mcp initialize {}", initialize);
return mcpSyncClient;
}
throw new RuntimeException("tool mcp sse and stdio is null!");
}
private ToolCallback[] createToolCallbacks(List<McpSyncClient> mcpSyncClients) throws Exception {
List<ToolCallback> toolCallbacks = new ArrayList<>();
for (McpSyncClient mcpSyncClient : mcpSyncClients) {
String cursor = null;
do {
McpSchema.ListToolsResult listToolsResult = StringUtils.isBlank(cursor)
? mcpSyncClient.listTools()
: mcpSyncClient.listTools(cursor);
for (McpSchema.Tool tool : listToolsResult.tools()) {
toolCallbacks.add(new SafeMcpToolCallback(mcpSyncClient, tool));
}
cursor = listToolsResult.nextCursor();
} while (StringUtils.isNotBlank(cursor));
}
return toolCallbacks.toArray(new ToolCallback[0]);
}
private static class SafeMcpToolCallback implements ToolCallback {
private final McpSyncClient mcpSyncClient;
private final McpSchema.Tool tool;
private final ToolDefinition toolDefinition;
private SafeMcpToolCallback(McpSyncClient mcpSyncClient, McpSchema.Tool tool) throws Exception {
this.mcpSyncClient = mcpSyncClient;
this.tool = tool;
this.toolDefinition = ToolDefinition.builder()
.name(tool.name())
.description(tool.description())
.inputSchema(OBJECT_MAPPER.writeValueAsString(tool.inputSchema()))
.build();
}
@Override
public ToolDefinition getToolDefinition() {
return toolDefinition;
}
@Override
public ToolMetadata getToolMetadata() {
return ToolCallback.super.getToolMetadata();
}
@Override
public String call(String toolInput) {
try {
return doCall(toolInput);
} catch (Exception e) {
return fallback(e);
}
}
@Override
public String call(String toolInput, ToolContext toolContext) {
try {
return doCall(toolInput);
} catch (Exception e) {
return fallback(e);
}
}
private String doCall(String toolInput) throws Exception {
Map<String, Object> arguments = StringUtils.isBlank(toolInput)
? Collections.emptyMap()
: OBJECT_MAPPER.readValue(toolInput, new TypeReference<Map<String, Object>>() {
});
McpSchema.CallToolResult callToolResult = mcpSyncClient.callTool(
new McpSchema.CallToolRequest(tool.name(), arguments));
return OBJECT_MAPPER.writeValueAsString(callToolResult);
}
private String fallback(Exception e) {
String toolName = tool.name();
log.warn("MCP tool call failed, toolName={}, message={}", toolName, e.getMessage());
return "{\"error\":\"MCP_TOOL_CALL_FAILED\",\"tool\":\"" + escapeJson(toolName)
+ "\",\"message\":\"" + escapeJson(e.getMessage()) + "\"}";
}
private String escapeJson(String value) {
if (null == value) {
return "";
}
return value.replace("\\", "\\\\").replace("\"", "\\\"");
}
}
}- 原先的实现依赖大量的 if 判断,新增一个 local 本地选项后,判断逻辑会进一步增加,导致代码难以维护,类的体量也过大。因此,需要对这部分逻辑进行优化和重构。
2.3.2 策略接口(优化)
public interface ToolMcpCreateService {
ToolCallback[] buildToolCallback(AiAgentConfigTableVO.Module.ChatModel.ToolMcp toolMcp);
}- 策略接口是一种面向扩展的抽象设计模式,通过定义统一的接口规范,使不同的实现类都能够以一致的方式接收参数并返回结果。具体的处理逻辑由各个实现类自行完成,对外只暴露统一的调用方式,从而有效隔离实现差异,降低模块间的耦合度,并提升系统的可维护性和扩展性。
2.3.3 策略实现
mcpsse
public class SSEToolMcpCreateService implements TooMcpCreateService {
@Override
public ToolCallback[] buildToolCallback(AiAgentConfigTableVO.Module.ChatModel.ToolMcp toolMcp) throws Exception {
AiAgentConfigTableVO.Module.ChatModel.ToolMcp.SSEServerParameters sseConfig = toolMcp.getSse();
// http://appbuilder.baidu.com/v2/ai_search/mcp/sse?api_key=bce-v3/ALTAK-JFZXXLpfxhAutDQvJ32Ei/4492c1879b8c2f0df4612ef5b4a52df1c1fba9f7
String originalBaseUri = sseConfig.getBaseUri();
String baseUri = originalBaseUri;
String sseEndpoint = sseConfig.getSseEndpoint();
if (StringUtils.isBlank(sseEndpoint)) {
URL url = new URL(originalBaseUri);
String protocol = url.getProtocol();
String host = url.getHost();
int port = url.getPort();
String baseUrl = port == -1 ? protocol + "://" + host : protocol + "://" + host + ":" + port;
int index = originalBaseUri.indexOf(baseUrl);
if (index != -1) {
sseEndpoint = originalBaseUri.substring(index + baseUrl.length());
}
baseUri = baseUrl;
}
sseEndpoint = StringUtils.isBlank(sseEndpoint) ? "/sse" : sseEndpoint;
HttpClientSseClientTransport sseClientTransport = HttpClientSseClientTransport.builder(baseUri).sseEndpoint(sseEndpoint).build();
McpSyncClient mcpSyncClient = McpClient.sync(sseClientTransport).requestTimeout(Duration.ofMillis(sseConfig.getRequestTimeout())).build();
McpSchema.InitializeResult initialize = mcpSyncClient.initialize();
log.info("tool sse mcp initialize {}", initialize);
return createToolCallbacks(mcpSyncClient);
}
private ToolCallback[] createToolCallbacks(McpSyncClient mcpSyncClient) throws Exception {
List<ToolCallback> toolCallbacks = new ArrayList<>();
String cursor = null;
do {
McpSchema.ListToolsResult listToolsResult = StringUtils.isBlank(cursor) ? mcpSyncClient.listTools() : mcpSyncClient.listTools(cursor);
for (McpSchema.Tool tool : listToolsResult.tools()) {
toolCallbacks.add(new SSEToolMcpCreateService.SafeMcpToolCallback(mcpSyncClient, tool));
}
cursor = listToolsResult.nextCursor();
} while (StringUtils.isNotBlank(cursor));
return toolCallbacks.toArray(new ToolCallback[0]);
}
private static class SafeMcpToolCallback implements ToolCallback {
private final McpSyncClient mcpSyncClient;
private final McpSchema.Tool tool;
private final ToolDefinition toolDefinition;
private SafeMcpToolCallback(McpSyncClient mcpSyncClient, McpSchema.Tool tool) throws Exception {
this.mcpSyncClient = mcpSyncClient;
this.tool = tool;
this.toolDefinition = ToolDefinition.builder().name(tool.name()).description(tool.description()).inputSchema(OBJECT_MAPPER.writeValueAsString(tool.inputSchema())).build();
}
@Override
public ToolDefinition getToolDefinition() {
return toolDefinition;
}
@Override
public ToolMetadata getToolMetadata() {
return ToolCallback.super.getToolMetadata();
}
@Override
public String call(String toolInput) {
try {
return doCall(toolInput);
} catch (Exception e) {
return fallback(e);
}
}
@Override
public String call(String toolInput, ToolContext toolContext) {
try {
return doCall(toolInput);
} catch (Exception e) {
return fallback(e);
}
}
private String doCall(String toolInput) throws Exception {
Map<String, Object> arguments = StringUtils.isBlank(toolInput) ? Collections.emptyMap() : OBJECT_MAPPER.readValue(toolInput, new TypeReference<Map<String, Object>>() {
});
McpSchema.CallToolResult callToolResult = mcpSyncClient.callTool(new McpSchema.CallToolRequest(tool.name(), arguments));
return OBJECT_MAPPER.writeValueAsString(callToolResult);
}
private String fallback(Exception e) {
String toolName = tool.name();
log.warn("MCP tool call failed, toolName={}, message={}", toolName, e.getMessage());
return "{\"error\":\"MCP_TOOL_CALL_FAILED\",\"tool\":\"" + escapeJson(toolName) + "\",\"message\":\"" + escapeJson(e.getMessage()) + "\"}";
}
private String escapeJson(String value) {
if (null == value) {
return "";
}
return value.replace("\\", "\\\\").replace("\"", "\\\"");
}
}
}关键设计点:
配置自适应:sseEndpoint 缺省时会从 baseUri 自动拆分,兼容用户直接把完整 SSE URL 配置在 baseUri 的写法(注释里给出了百度 AppBuilder 的示例 URL)。
分页拉取:通过 nextCursor 兼容工具数量较多的 MCP 服务端。
容错隔离:工具调用失败不会抛异常打断模型对话,而是返回结构化 JSON 错误,模型可基于错误信息自行决定后续动作。
协议桥接:把 MCP 的 Tool / CallToolRequest / CallToolResult 适配为 Spring AI 的 ToolCallback / ToolDefinition 体系。
mcpstdio
@Service
public class StdioToolMcpCreateService implements ToolMcpCreateService {
@Override
public ToolCallback[] buildToolCallback(AiAgentConfigTableVO.Module.ChatModel.ToolMcp toolMcp) {
AiAgentConfigTableVO.Module.ChatModel.ToolMcp.StdioServerParameters stdioConfig = toolMcp.getStdio();
AiAgentConfigTableVO.Module.ChatModel.ToolMcp.StdioServerParameters.ServerParameters serverParameters = stdioConfig.getServerParameters();
// https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem
var stdioParams = ServerParameters.builder(serverParameters.getCommand())
.args(serverParameters.getArgs())
.env(serverParameters.getEnv())
.build();
var mcpClient = McpClient.sync(new StdioClientTransport(stdioParams, new JacksonMcpJsonMapper(new ObjectMapper())))
.requestTimeout(Duration.ofSeconds(stdioConfig.getRequestTimeout())).build();
var init_stdio = mcpClient.initialize();
log.info("Tool Stdio MCP Initialized {}", init_stdio);
return SyncMcpToolCallbackProvider.builder().mcpClients(mcpClient).build()
.getToolCallbacks();
}
}- 把源码代码,迁移过来,返回
SyncMcpToolCallbackProvider.builder().mcpClients(mcpClient).build().getToolCallbacks()
mcplocal
@Service
public class LocalToolMcpCreateService implements ToolMcpCreateService {
@Resource
protected ApplicationContext applicationContext;
@Override
public ToolCallback[] buildToolCallback(AiAgentConfigTableVO.Module.ChatModel.ToolMcp toolMcp) {
AiAgentConfigTableVO.Module.ChatModel.ToolMcp.LocalParameters local = toolMcp.getLocal();
ToolCallbackProvider localToolCallbackProvider = (ToolCallbackProvider) applicationContext.getBean(local.getName());
log.info("Tool Local MCP Initialized {}", local.getName());
return localToolCallbackProvider.getToolCallbacks();
}
}- 本地 local 核心的就是拿到 bean 的名称,也就顺便拿到了 getToolCallbacks 返回即可。
2.4 工厂调用
2.4.1 定义工厂
@Slf4j
@Service
public class DefaultMcpClientFactory {
@Resource
private LocalToolMcpCreateService localToolMcpCreateService;
@Resource
private SSEToolMcpCreateService sseToolMcpCreateService;
@Resource
private StdioToolMcpCreateService stdioToolMcpCreateService;
public ToolMcpCreateService getToolMcpCreateService(AiAgentConfigTableVO.Module.ChatModel.ToolMcp toolMcp) {
if (null != toolMcp.getLocal()) return localToolMcpCreateService;
if (null != toolMcp.getSse()) return sseToolMcpCreateService;
if (null != toolMcp.getStdio()) return stdioToolMcpCreateService;
throw new AppException(ResponseCode.NOT_FOUND_METHOD.getCode(), ResponseCode.NOT_FOUND_METHOD.getInfo());
}
}DefaultMcpClientFactory工厂类负责根据用户配置的toolMcp信息,匹配并返回对应的 MCP 客户端服务,后续由调用方自行完成具体的调用操作。如果希望进一步减少条件分支判断,也可以采用“枚举 + 策略”的设计方式。在枚举中维护 MCP 类型与对应服务标识的映射关系(可通过扩展字段实现),然后在
DefaultMcpClientFactory中以Map的形式注入所有 MCP 服务实现。当用户传入 MCP 类型后,先通过枚举获取对应的服务标识,再从Map中获取目标服务实例并返回。这样不仅能够消除大量if...else判断,还能让新增 MCP 类型时只需扩展实现类和枚举配置,无需修改工厂逻辑。
2.4.2 使用工厂
public class ChatModelNode extends AbstractArmorySupport {
@Resource
private AgentNode agentNode;
@Resource
private DefaultMcpClientFactory defaultMcpClientFactory;
/**
* 装配 ChatModel 节点(Ai Agent 装配链的第二个业务节点:AiApiNode → ChatModelNode → AgentNode)。
* <p>
* 基于上游 {@link AiApiNode} 放入动态上下文的 {@link OpenAiApi},结合聊天模型配置
* (模型名称、绑定的 MCP 工具列表)构建出可对话的 {@link ChatModel},并写回动态上下文,
* 供后续 {@link AgentNode} 构建 Agent 时使用。
*
* @param requestParameter 装配命令实体,携带 Agent 的完整配置({@link AiAgentConfigTableVO})
* @param dynamicContext 装配动态上下文,用于在各节点间传递中间产物(如 OpenAiApi、ChatModel)
* @return 装配结果,由 {@code router(...)} 透传下游节点的处理结果
* @throws Exception 构建 MCP 工具回调或 ChatModel 过程中可能抛出的异常
*/
@Override
protected AiAgentRegisterVO doApply(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
log.info("Ai Agent 装配操作 - ChatModelNode");
// 取出上游 AiApiNode 已构建并放入上下文的 OpenAiApi(包含 baseUrl、apiKey 等连接信息)
OpenAiApi openAiApi = dynamicContext.getOpenAiApi();
// 获取聊天模型配置:模型名称及其绑定的 MCP 工具列表
AiAgentConfigTableVO aiAgentConfigTableVO = requestParameter.getAiAgentConfigTableVO();
AiAgentConfigTableVO.Module.ChatModel chatModelConfig = aiAgentConfigTableVO.getModule().getChatModel();
List<AiAgentConfigTableVO.Module.ChatModel.ToolMcp> toolMcpList = chatModelConfig.getToolMcpList();
// 遍历 MCP 工具配置,通过工厂创建对应的 MCP 客户端并构建 ToolCallback,汇总为模型可调用的工具集合
List<ToolCallback> toolCallbackList = new ArrayList<>();
for (AiAgentConfigTableVO.Module.ChatModel.ToolMcp toolMcp : toolMcpList) {
ToolMcpCreateService tooMcpCreateService = defaultMcpClientFactory.getToolMcpCreateService(toolMcp);
ToolCallback[] toolCallbacks = tooMcpCreateService.buildToolCallback(toolMcp);
toolCallbackList.addAll(List.of(toolCallbacks));
}
// 基于 OpenAiApi、模型名称与工具回调集合构建 ChatModel
ChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(OpenAiChatOptions.builder()
.model(chatModelConfig.getModel())
.toolCallbacks(toolCallbackList)
.build())
.build();
// 将构建好的 ChatModel 写回动态上下文,供下游 AgentNode 等节点装配使用
dynamicContext.setChatModel(chatModel);
// 路由到下一个策略节点(即 get() 返回的 AgentNode)继续装配
return router(requestParameter, dynamicContext);
}
/**
* 返回当前节点在装配链中的下一个策略处理器。
* <p>
* 由父类 {@code AbstractMultiThreadStrategyRouter} 的 {@code router(...)} 回调,
* 驱动责任链向后流转。ChatModelNode 的下游固定为 {@link AgentNode}。
*
* @param requestParameter 装配命令实体
* @param dynamicContext 装配动态上下文
* @return 下一个待执行的策略处理器({@link AgentNode})
* @throws Exception 获取下游处理器过程中可能抛出的异常
*/
@Override
public StrategyHandler<ArmoryCommandEntity, DefaultArmoryFactory.DynamicContext, AiAgentRegisterVO> get(ArmoryCommandEntity requestParameter, DefaultArmoryFactory.DynamicContext dynamicContext) throws Exception {
return agentNode;
}
}遍历
toolMcpList,通过defaultMcpClientFactory获取各类ToolMcpCreateService实现(如 sse、stdio、local)的服务实例。处理完成后,将所有结果统一收集到
ToolCallback[] toolCallbacks中,存入toolCallbackList,保证数据结构整齐统一 !
2.5 自定义服务(mcp)
2.5.1 服务案例
@Slf4j
@Service
public class MyTestMcpService {
@Tool(description = "小写字母转换为大写字母")
public XxxResponse toUpperCase(XxxRequest request) {
XxxResponse xxxResponse = new XxxResponse();
xxxResponse.setContent(request.getWord().toUpperCase());
return xxxResponse;
}
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class XxxRequest {
@JsonProperty(required = true, value = "word")
@JsonPropertyDescription("英文单词,字符串,字母。例如: good,cactusli")
private String word;
}
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class XxxResponse {
@JsonProperty(required = true, value = "content")
@JsonPropertyDescription("单词转换结果")
private String content;
}
}2.5.2 服务装配
public class Application {
public static void main(String[] args){
SpringApplication.run(Application.class);
}
@Bean("myToolCallbackProvider")
public ToolCallbackProvider testTools(MyTestMcpService testService) {
return MethodToolCallbackProvider.builder().toolObjects(testService).build();
}
}- 在 Application 下完成装配处理,bean 的名称
myToolCallbackProvider就是作为本地的 mcp 服务进行使用的。
四、测试验证
1. yml 配置(only-one-agent.yml)
ai:
agent:
config:
tables:
testAgent03:
app-name: testAgent03
agent:
agent-id: 100003
agent-name: 单一智能体
agent-desc: 单一智能体
module:
ai-api:
base-url: https://api1.oaipro.com
api-key: sk-xxx
completions-path: v1/chat/completions
embeddings-path: v1/embeddings
chat-model:
model: gpt-4.1
tool-mcp-list:
- sse:
name: baidu-search
base-uri: http://appbuilder.baidu.com/v2/ai_search/mcp/
sse-endpoint: sse?api_key=bce-v3/ALTAK-6SyOtHHftyPsWLoIpz3WL/e7a0d8c2e39e9596b0f1e183cd985da1c32a0d67
request-timeout: 50000
- local:
name: myToolCallbackProvider
agents:
- name: onlyAgent
description: Spring Boot重磅学习计划
instruction: |
通过百度检索Spring Boot编程实战项目,并根据检索内容,针对初学用户给出学习计划。
runner:
agent-name: onlyAgent- 在 only-one-agent.yml 下,增加一个 local 配置,指定 bean 的名称。
public class AiAgentAutoConfigTest {
@Resource
private ApplicationContext applicationContext;
@Test
public void test_agent() throws InterruptedException {
AiAgentRegisterVO aiAgentRegisterVO = applicationContext.getBean("100001", AiAgentRegisterVO.class);
String appName = aiAgentRegisterVO.getAppName();
InMemoryRunner runner = aiAgentRegisterVO.getRunner();
Session session = runner.sessionService()
.createSession(appName, "cactusli")
.blockingGet();
Content userMsg = Content.fromParts(Part.fromText("编写冒泡排序,加上注释!"));
Flowable<Event> events = runner.runAsync("cactusli", session.id(), userMsg);
List<String> outputs = new ArrayList<>();
events.blockingForEach(event -> outputs.add(event.stringifyContent()));
log.info("测试结果:{}", JSON.toJSONString(outputs));
new CountDownLatch(1).await();
}
@Test
public void test_handlerMessage_03(){
AiAgentRegisterVO aiAgentRegisterVO = applicationContext.getBean("100003", AiAgentRegisterVO.class);
String appName = aiAgentRegisterVO.getAppName();
InMemoryRunner runner = aiAgentRegisterVO.getRunner();
Session session = runner.sessionService()
.createSession(appName, "cactusli")
.blockingGet();
Content userMsg = Content.fromParts(Part.fromText("给我一份Spring Boot学习计划"));
Flowable<Event> events = runner.runAsync("cactusli", session.id(), userMsg);
List<String> outputs = new ArrayList<>();
events.blockingForEach(event -> outputs.add(event.stringifyContent()));
log.info("测试结果:{}", JSON.toJSONString(outputs));
}
@Test
public void test_handlerMessage_02(){
AiAgentRegisterVO aiAgentRegisterVO = applicationContext.getBean("100003", AiAgentRegisterVO.class);
String appName = aiAgentRegisterVO.getAppName();
InMemoryRunner runner = aiAgentRegisterVO.getRunner();
Session session = runner.sessionService()
.createSession(appName, "cactusli")
.blockingGet();
// Content userMsg = Content.fromParts(Part.fromText("你具备哪些能力?"));
Content userMsg = Content.fromParts(Part.fromText("有哪些工具可以使用?"));
Flowable<Event> events = runner.runAsync("cactusli", session.id(), userMsg);
List<String> outputs = new ArrayList<>();
events.blockingForEach(event -> outputs.add(event.stringifyContent()));
log.info("测试结果:{}", JSON.toJSONString(outputs));
}
@Test
public void test_handlerMessage_04(){
AiAgentRegisterVO aiAgentRegisterVO = applicationContext.getBean("100003", AiAgentRegisterVO.class);
String appName = aiAgentRegisterVO.getAppName();
InMemoryRunner runner = aiAgentRegisterVO.getRunner();
Session session = runner.sessionService()
.createSession(appName, "cactusli")
.blockingGet();
Content userMsg = Content.fromParts(Part.fromText("把cactusli转换为大写"));
Flowable<Event> events = runner.runAsync("cactusli", session.id(), userMsg);
List<String> outputs = new ArrayList<>();
events.blockingForEach(event -> outputs.add(event.stringifyContent()));
log.info("测试结果:{}", JSON.toJSONString(outputs));
}
}26-06-05.15:39:15.157 [main ] INFO SpringAIObservabilityHandler - Request completed successfully: model=openai, type=chat, duration=5357ms, tokens=2254
26-06-05.15:39:15.163 [main ] INFO AiAgentAutoConfigTest - 测试结果:["cactusli 转换为大写是:CACTUSLI"]