Junki
Junki
Published on 2025-12-30 / 14 Visits
0
0

动态注册用户自定义 MCP 工具的实现思路

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"
    }
  }
}

效果验证:

微信图片_20251230112816_98_188.png


Comment