1. 需求概要
现有一个MCP Server,需要实现用户自定义工具的动态注册。
- 用户输入:工具名称、标题、描述、输入字段描述、工具代码块。
- MCP Server需要动态注册这个工具,并通知MCP Client。
- MCP Server需要执行工具代码块。
2. 实现思路
2.1 工具的动态注册
使用 spring-ai-alibaba 轻松实现。
spring-ai-alibaba开源地址:https://github.com/alibaba/spring-ai-alibaba
2.2 执行工具代码块
常见的实现思路是启用一个python虚拟环境,用户输入的工具代码块是python脚本,服务端在虚拟环境中运行python脚本。
本文讲解另一种思路,结合 QLExpress 和 预定义函数 来实现,可降低系统复杂度、简化用户编码、提高服务安全性。
QLExpress开源地址:https://github.com/alibaba/QLExpress
3. 实现过程
3.1 创建 MCP Server 工程
pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.7</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>cn.junki</groupId>
<artifactId>mcp-center</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mcp-center</name>
<description>MCP Center</description>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.1.0</spring-ai.version>
<spring-ai-alibaba-extensions.version>1.1.0.0-RC2</spring-ai-alibaba-extensions.version>
<spring-ai-alibaba.version>1.1.0.0-RC2</spring-ai-alibaba.version>
<hutool.version>5.8.42</hutool.version>
<qlexpress.version>4.0.7</qlexpress.version>
<javax.annotation.version>1.3.2</javax.annotation.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-extensions-bom</artifactId>
<version>${spring-ai-alibaba-extensions.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>${spring-ai-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>qlexpress4</artifactId>
<version>${qlexpress.version}</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>${javax.annotation.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>qlexpress4</artifactId>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yaml如下:
spring:
ai:
mcp:
server:
protocol: STREAMABLE
server:
port: 8099
3.2 创建 qlexpress 执行器业务类
创建执行器service:
package cn.junki.mcpcenter.service;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.qlexpress4.Express4Runner;
import com.alibaba.qlexpress4.InitOptions;
import com.alibaba.qlexpress4.QLOptions;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Map;
/**
* 表达式执行服务
*
* @author Junki
*/
@Service
public class ExpressionService {
private Express4Runner express4Runner;
@PostConstruct
public void init() {
// 初始化表达式执行器
express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
// 注册自定义函数
registerFunctions();
}
/**
* 注册自定义函数
*/
private void registerFunctions() {
// 注册http_post请求函数
express4Runner.addFunction("http_post", (qContext, parameters) -> {
if (null == parameters.get(0) || StrUtil.isBlank(parameters.get(0).get().toString())) {
return "url cannot be empty";
}
String url = parameters.get(0).get().toString();
Object body = parameters.get(1) != null ? parameters.get(1).get() : null;
return HttpUtil.post(url, JSONUtil.toJsonStr(body));
});
}
/**
* 执行表达式
*
* @param expression 表达式字符串
* @param context 上下文变量
* @return 执行结果
*/
public Object execute(String expression, Map<String, Object> context) {
if (expression == null || expression.trim().isEmpty()) {
throw new IllegalArgumentException("表达式不能为空");
}
try {
return express4Runner.execute(expression, context, QLOptions.DEFAULT_OPTIONS).getResult();
} catch (Exception e) {
throw new RuntimeException("表达式执行失败: " + e.getMessage(), e);
}
}
}
主要逻辑:
- 初始化Express4Runner
- 注册http_post函数
3.3 模拟用户注册工具动态加载
在启动监听器中模拟注册过程:
package cn.junki.mcpcenter.listener;
import cn.junki.mcpcenter.service.ExpressionService;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.spec.McpSchema;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import java.util.List;
import java.util.stream.Collectors;
/**
* 应用就绪监听
*
* @author Junki
* @since 2025-12-19
*/
@Slf4j
@Configuration
public class ApplicationReady {
@Resource
@Lazy
private McpSyncServer mcpSyncServer;
@Resource
private ExpressionService expressionService;
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
// 新增用户自定义工具
mcpSyncServer.addTool(McpServerFeatures.SyncToolSpecification.builder()
.tool(McpSchema.Tool.builder()
.name("search-junki-blog")
.title("Search Junki Blog")
.description("tool of search junki blog")
.inputSchema(McpJsonMapper.getDefault(), """
{
"type": "object",
"required": ["keyword"],
"properties": {
"keyword": {
"type": "string",
"description": "keyword of search"
}
}
}
""")
.build())
.callHandler((exchange, req) -> {
String respBody = expressionService.execute("""
body = {
"highlightPostTag": "</mark>",
"highlightPreTag": "<mark>",
"keyword": keyword
}
return http_post('https://junki.cn/apis/api.halo.run/v1alpha1/indices/-/search', body)
""",
req.arguments())
.toString();
return McpSchema.CallToolResult.builder().addTextContent(respBody).build();
})
.build());
// 列出所有工具
List<McpSchema.Tool> tools = mcpSyncServer.listTools();
// 所有工具名称
String toolNames = tools.stream().map(McpSchema.Tool::name).collect(Collectors.joining(","));
log.info("MCP Tools: count=[{}];names=[{}]", tools.size(), toolNames);
}
}
工具名称为:search-junki-blog
工具标题为:Search Junki Blog
工具描述为:tool of search junki blog
工具输入定义为:
{
"type": "object",
"required": ["keyword"],
"properties": {
"keyword": {
"type": "string",
"description": "keyword of search"
}
}
}
工具代码块为:``
body = {
"highlightPostTag": "</mark>",
"highlightPreTag": "<mark>",
"keyword": keyword
}
return http_post('https://junki.cn/apis/api.halo.run/v1alpha1/indices/-/search', body)
其中关键部分均为字符串,实际应用时可由用户表单输入。
3.4 效果验证
Cursor中MCP配置:
{
"mcpServers": {
"mcp-center": {
"url": "http://127.0.0.1:8099/mcp"
}
}
}
效果验证:
