Solon v3.10.0

mcp - 服务端根据权限分组输出 tools

</> markdown
2026年3月22日 上午10:44:04

从 mcp-sdk(java) 的角度,是没法实现动态输出 tools 的。mcp-sdk 的机制是先注册 tool,然后内部处理 "tools/list" 响应(而且特别内部)。要实现动态输出 tools ,一般有两种方案

  • 修改 mcp-sdk 代码,扩展 "tools/list" 处理,由外部代理(不利于 mcp-sdk 的未来维护与升级)
  • 先定义好 tools 组,然后在 http 接入处进行切换(这个是目前可以做的)

下面使用 http 接入处进行切换的方案,进行演示(v3.9.6 后支持)

1、容器风格

以下以 solon 容器为例。其它容器需要自己转下。

import io.modelcontextprotocol.server.transport.WebRxStatelessServerTransport;
import org.noear.solon.Solon;
import org.noear.solon.ai.annotation.PromptMapping;
import org.noear.solon.ai.annotation.ResourceMapping;
import org.noear.solon.ai.annotation.ToolMapping;
import org.noear.solon.ai.chat.message.ChatMessage;
import org.noear.solon.ai.chat.prompt.MethodPromptProvider;
import org.noear.solon.ai.chat.resource.MethodResourceProvider;
import org.noear.solon.ai.chat.tool.MethodToolProvider;
import org.noear.solon.ai.mcp.McpChannel;
import org.noear.solon.ai.mcp.server.McpServerEndpointProvider;
import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Param;
import org.noear.solon.core.handle.Context;

import java.util.Arrays;
import java.util.Collection;

public class App {
    public static void main(String[] args) {
        Solon.start(App.class, args);

        WebRxStatelessServerTransport serverTransport1 = buildMcpServerTransport("group1");
        WebRxStatelessServerTransport serverTransport2 = buildMcpServerTransport("group2");

        //接入时进行路由分发(不同的 ServerTransport,需要代理不同的方法。可以看它的 toHttpHandler 方法里有哪些处理)
        Solon.app().router().get("/mcp", ctx -> {
            int role = ctx.paramAsInt("role");

            if (role == 1) {
                serverTransport1.handleGet(ctx);
            } else {
                serverTransport2.handleGet(ctx);
            }
        });

        Solon.app().router().post("/mcp", ctx -> {
            int role = ctx.paramAsInt("role");

            if (role == 1) {
                serverTransport1.handlePost(ctx);
            } else {
                serverTransport2.handlePost(ctx);
            }
        });
    }

    public static WebRxStatelessServerTransport buildMcpServerTransport(String group) {
        Object mcpBean = Solon.context().getBean(group);

        if (mcpBean == null) {
            return null;
        }

        McpServerEndpointProvider endpointProvider = McpServerEndpointProvider.builder()
                .name(group)
                .channel(McpChannel.STREAMABLE_STATELESS)
                .sseEndpoint("/mcp")
                .build();

        //不同的 group ,用不同的 tools
        endpointProvider.addTool(new MethodToolProvider(mcpBean));
        endpointProvider.addResource(new MethodResourceProvider(mcpBean));
        endpointProvider.addPrompt(new MethodPromptProvider(mcpBean));

        //这里用 build(不用 start,否则会自动注册到路由器)
        //(WebRxStatelessServerTransport) 要与 .channel(McpChannel.STREAMABLE_STATELESS) 对应起来
        return (WebRxStatelessServerTransport) endpointProvider.getServer().build();
    }

    @Component("group1")
    public static class McpServerTool {
        //@Inject //容器形态可以注入(否则,要自己组件需要的服务)
        //UserService userService;

        @ToolMapping(description = "查询天气预报")
        public String getWeather(@Param(description = "城市位置") String location, Context ctx) {
            return "晴,14度";
        }

        @ResourceMapping(uri = "config://app-version", description = "获取应用版本号", mimeType = "text/config")
        public String getAppVersion() {
            return "v3.2.0";
        }

        @PromptMapping(description = "生成关于某个主题的提问")
        public Collection<ChatMessage> askQuestion(@Param(description = "主题") String topic) {
            return Arrays.asList(
                    ChatMessage.ofUser("请解释一下'" + topic + "'的概念?")
            );
        }
    }
}

2、Java 原生风格

import io.modelcontextprotocol.server.transport.WebRxStatelessServerTransport;
import org.noear.solon.Solon;
import org.noear.solon.ai.annotation.PromptMapping;
import org.noear.solon.ai.annotation.ResourceMapping;
import org.noear.solon.ai.annotation.ToolMapping;
import org.noear.solon.ai.chat.message.ChatMessage;
import org.noear.solon.ai.chat.prompt.MethodPromptProvider;
import org.noear.solon.ai.chat.resource.MethodResourceProvider;
import org.noear.solon.ai.chat.tool.MethodToolProvider;
import org.noear.solon.ai.mcp.McpChannel;
import org.noear.solon.ai.mcp.server.McpServerEndpointProvider;
import org.noear.solon.annotation.Param;
import org.noear.solon.core.handle.Context;

import java.util.Arrays;
import java.util.Collection;

public class App {
    public static void main(String[] args) {
        Solon.start(App.class, args);

        WebRxStatelessServerTransport serverTransport1 = buildMcpServerTransport("group1");
        WebRxStatelessServerTransport serverTransport2 = buildMcpServerTransport("group2");

        //接入时进行路由分发(不同的 ServerTransport,需要代理不同的方法。可以看它的 toHttpHandler 方法里有哪些处理)
        Solon.app().router().get("/mcp", ctx -> {
            int role = ctx.paramAsInt("role");

            if (role == 1) {
                serverTransport1.handleGet(ctx);
            } else {
                serverTransport2.handleGet(ctx);
            }
        });

        Solon.app().router().post("/mcp", ctx -> {
            int role = ctx.paramAsInt("role");

            if (role == 1) {
                serverTransport1.handlePost(ctx);
            } else {
                serverTransport2.handlePost(ctx);
            }
        });
    }

    private static WebRxStatelessServerTransport buildMcpServerTransport(String group) {
        McpServerEndpointProvider endpointProvider = McpServerEndpointProvider.builder()
                .name(group)
                .channel(McpChannel.STREAMABLE_STATELESS)
                .sseEndpoint("/mcp")
                .build();

        //不同的 group ,用不同的 tools
        endpointProvider.addTool(new MethodToolProvider(new McpServerTool()));
        endpointProvider.addResource(new MethodResourceProvider(new McpServerTool()));
        endpointProvider.addPrompt(new MethodPromptProvider(new McpServerTool()));

        //这里用 build(不用 start,否则会自动注册到路由器)
        //(WebRxStatelessServerTransport) 要与 .channel(McpChannel.STREAMABLE_STATELESS) 对应起来
        return (WebRxStatelessServerTransport) endpointProvider.getServer().build();
    }

    private static class McpServerTool {
        @ToolMapping(description = "查询天气预报")
        public String getWeather(@Param(description = "城市位置") String location, Context ctx) {
            return "晴,14度";
        }

        @ResourceMapping(uri = "config://app-version", description = "获取应用版本号", mimeType = "text/config")
        public String getAppVersion() {
            return "v3.2.0";
        }

        @PromptMapping(description = "生成关于某个主题的提问")
        public Collection<ChatMessage> askQuestion(@Param(description = "主题") String topic) {
            return Arrays.asList(
                    ChatMessage.ofUser("请解释一下'" + topic + "'的概念?")
            );
        }
    }
}