用 Solon AI 从零构建 MCP 工具服务:让 AI Agent 拥有真实世界的能力

当 AI Agent 需要查询天气、搜索文档、调用 API 时,它需要一个标准化的工具调用协议。MCP(Model Context Protocol)正是为此而生------它由 Anthropic 提出,正在成为 AI 工具生态的"USB Type-C"。

Solon AI 提供了完整的 MCP 支持:

  • ✅ 服务端开发:@McpServerEndpoint + @ToolMapping
  • ✅ 客户端集成:McpClientProvider + ChatModel
  • ✅ 四种传输通道:Streamable / SSE / STDIO / Streamable Stateless
  • ✅ Java 8 ~ Java 25 全兼容
  • ✅ 可嵌入第三方框架(SpringBoot、jFinal、Vert.x)

一、MCP 是什么?

1.1 一句话理解

MCP 是 AI 模型与外部世界交互的标准化协议。就像 USB 统一了设备接口,MCP 统一了 AI 工具接口。

1.2 三种原语

原语 作用 类比
Tools 让 AI 调用函数执行操作 远程过程调用
Resources 让 AI 读取结构化数据 文件/数据读取
Prompts 提供可复用的提示模板 模板引擎

1.3 为什么选 MCP?

  • 标准化:同一套协议,不同模型、不同工具都能互联互通
  • 自动发现:客户端可以动态查询服务端提供了哪些工具
  • 跨模型复用:工具服务只需开发一次,所有模型都能用
  • 生态丰富:Cursor、Claude Desktop、各大 IDE 都已支持

二、快速入门:5 分钟构建 MCP 服务

2.1 添加依赖

pom.xml 中引入 Solon AI MCP:

xml 复制代码
<dependency>
    <groupId>org.noear</groupId>
    <artifactId>solon-ai-mcp</artifactId>
</dependency>

支持 Java 8、11、17、21、25、26。

2.2 Hello World 服务端

java 复制代码
import org.noear.solon.Solon;
import org.noear.solon.ai.annotation.ToolMapping;
import org.noear.solon.ai.mcp.McpChannel;
import org.noear.solon.ai.mcp.server.annotation.McpServerEndpoint;
import org.noear.solon.annotation.Param;

@McpServerEndpoint(channel = McpChannel.STREAMABLE, mcpEndpoint = "/mcp")
public class HelloTool {

    @ToolMapping(description = "打招呼")
    public String hello(@Param(description = "名字") String name) {
        return "你好," + name + "!欢迎使用 Solon AI MCP 🎉";
    }
}

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

2.3 Hello World 客户端

java 复制代码
import org.noear.solon.ai.mcp.McpChannel;
import org.noear.solon.ai.mcp.client.McpClientProvider;

import java.util.Map;

public class McpClientTest {
    public static void main(String[] args) {
        McpClientProvider client = McpClientProvider.builder()
                .channel(McpChannel.STREAMABLE)
                .url("http://localhost:8080/mcp")
                .build();

        String result = client.callTool("hello", Map.of("name", "阿飞")).getContent();

        System.out.println(result); // 你好,阿飞!欢迎使用 Solon AI MCP 🎉
    }
}

就是这么简单!一个注解 @McpServerEndpoint,你的普通 Java 方法就变成了 MCP 工具服务。

三、三种原语开发详解

3.1 Tool(工具调用)

Tool 是 MCP 最常用的原语,让 AI 可以调用外部函数。

java 复制代码
@McpServerEndpoint(channel = McpChannel.STREAMABLE, mcpEndpoint = "/mcp")
public class WeatherTool {

    @ToolMapping(description = "查询指定城市的天气预报")
    public String getWeather(
            @Param(description = "城市名称") String city,
            @Param(description = "日期,格式 yyyy-MM-dd,默认今天") String date
    ) {
        // 模拟天气数据
        return String.format("%s %s:晴,14°C,东风2级", city, date);
    }

    @ToolMapping(description = "查询空气质量指数")
    public String getAirQuality(
            @Param(description = "城市名称") String city
    ) {
        return String.format("%s AQI:52(良),PM2.5:35", city);
    }
}

要点:

  • @ToolMapping(description=...):描述工具的用途,AI 根据此描述决定是否调用
  • @Param(description=...):描述参数含义,帮助 AI 正确传参
  • 建议开启编译参数 -parameters,让参数名自动识别

3.2 Resource(资源读取)

Resource 让 AI 可以读取结构化数据源。

java 复制代码
import org.noear.solon.ai.annotation.ResourceMapping;

@McpServerEndpoint(channel = McpChannel.STREAMABLE, mcpEndpoint = "/mcp")
public class DocResource {

    @ResourceMapping(uri = "docs://catalog")
    public String getCatalog() {
        return """
        # 知识库目录
        1. Solon 入门指南
        2. Solon AI 开发教程
        3. Solon Cloud 微服务实战
        4. Solon Flow 流程编排指南
        """;
    }

    @ResourceMapping(uri = "docs://{category}/{id}")
    public String getDoc(
            @Param(description = "分类") String category,
            @Param(description = "文档ID") String id
    ) {
        return String.format("文档内容:[%s] #%s", category, id);
    }
}

要点:

  • @ResourceMapping(uri=...):定义资源的 URI 模板
  • 支持 URI 模板参数(如 {category}/{id}
  • 客户端通过 readResource(uri) 读取

3.3 Prompt(提示模板)

Prompt 提供可复用的提示模板服务。

java 复制代码
import org.noear.solon.ai.annotation.PromptMapping;

@McpServerEndpoint(channel = McpChannel.STREAMABLE, mcpEndpoint = "/mcp")
public class QaPrompt {

    @PromptMapping(name = "code_review")
    public String codeReviewPrompt(
            @Param(description = "编程语言") String language,
            @Param(description = "代码内容") String code
    ) {
        return String.format("""
        你是一位资深的 %s 代码审查专家。请对以下代码进行审查:
        
        ```%s
        %s
        ```
        
        请从以下维度分析:
        1. 代码质量与可读性
        2. 潜在 Bug 与安全问题
        3. 性能优化建议
        4. 最佳实践建议
        """, language, language, code);
    }
}

要点:

  • @PromptMapping(name=...):定义提示模板的名称
  • 客户端通过 getPrompt(name) 获取模板内容
  • 适合标准化常见 AI 交互模式

四、四种传输通道

Solon AI MCP 支持四种传输方式,适配不同场景:

通道 适用场景 特点
STREAMABLE 生产环境首选 HTTP 双向流式,高性能
STREAMABLE_STATELESS 集群部署 无状态,支持负载均衡
SSE 兼容旧客户端 服务端推送事件
STDIO 本地进程通信 标准输入输出

配置示例

java 复制代码
// 推荐:Streamable HTTP(生产环境)
@McpServerEndpoint(channel = McpChannel.STREAMABLE, mcpEndpoint = "/mcp")

// 集群友好:无状态模式
@McpServerEndpoint(channel = McpChannel.STREAMABLE_STATELESS, mcpEndpoint = "/mcp")

// 兼容模式:SSE
@McpServerEndpoint(channel = McpChannel.SSE, mcpEndpoint = "/mcp/sse")

// 本地进程:STDIO
@McpServerEndpoint(channel = McpChannel.STDIO)

客户端匹配

java 复制代码
// 必须与服务端通道匹配
McpClientProvider client = McpClientProvider.builder()
        .channel(McpChannel.STREAMABLE)  // 匹配服务端的 STREAMABLE
        .url("http://localhost:8080/mcp")
        .build();

五、实战:企业知识库 MCP 工具服务

5.1 场景设计

我们要构建一个企业知识库的 MCP 服务,提供:

  • 3 个 Tool:搜索文档、获取详情、推荐相关文档
  • 1 个 Resource:知识库目录索引
  • 1 个 Prompt:专业问答模板

5.2 完整服务端代码

java 复制代码
package com.example.knowledge;

import org.noear.solon.Solon;
import org.noear.solon.ai.annotation.ToolMapping;
import org.noear.solon.ai.annotation.PromptMapping;
import org.noear.solon.ai.annotation.ResourceMapping;
import org.noear.solon.ai.mcp.McpChannel;
import org.noear.solon.ai.mcp.server.annotation.McpServerEndpoint;
import org.noear.solon.annotation.Param;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@McpServerEndpoint(channel = McpChannel.STREAMABLE, mcpEndpoint = "/mcp")
public class KnowledgeMcpServer {

    // ========== 模拟数据 ==========

    private static final List<String> CATALOG = Arrays.asList(
            "1. Solon 快速入门",
            "2. Solon IoC 容器详解",
            "3. Solon AI 开发指南",
            "4. Solon Cloud 微服务实战",
            "5. Solon Flow 流程编排",
            "6. Solon Data 数据访问",
            "7. Solon Security 安全认证"
    );

    private static final List<DocEntry> DOCS = Arrays.asList(
            new DocEntry("1", "Solon 快速入门", "入门", "Solon 是新一代 Java 应用开发框架..."),
            new DocEntry("2", "Solon IoC 容器详解", "核心", "Solon IoC 容器支持依赖注入..."),
            new DocEntry("3", "Solon AI 开发指南", "AI", "Solon AI 提供完整的大模型集成能力..."),
            new DocEntry("4", "Solon Cloud 微服务实战", "Cloud", "Solon Cloud 提供分布式套件..."),
            new DocEntry("5", "Solon Flow 流程编排", "Flow", "Solon Flow 是轻量级流程引擎..."),
            new DocEntry("6", "Solon Data 数据访问", "Data", "Solon Data 统一数据访问抽象..."),
            new DocEntry("7", "Solon Security 安全认证", "Security", "Solon Security 提供认证授权...")
    );

    // ========== Tool: 搜索文档 ==========

    @ToolMapping(description = "搜索知识库文档,支持关键词匹配")
    public String searchDocs(
            @Param(description = "搜索关键词") String keyword,
            @Param(description = "最大返回条数,默认3") int limit
    ) {
        List<DocEntry> results = DOCS.stream()
                .filter(d -> d.title.contains(keyword) || d.content.contains(keyword))
                .limit(limit > 0 ? limit : 3)
                .collect(Collectors.toList());

        if (results.isEmpty()) {
            return "未找到与「" + keyword + "」相关的文档";
        }

        StringBuilder sb = new StringBuilder("搜索结果:\n");
        for (DocEntry doc : results) {
            sb.append(String.format("- [%s] %s:%s\n", doc.id, doc.title, doc.content));
        }
        return sb.toString();
    }

    // ========== Tool: 获取文档详情 ==========

    @ToolMapping(description = "根据文档ID获取完整文档内容")
    public String getDocDetail(
            @Param(description = "文档ID") String docId
    ) {
        return DOCS.stream()
                .filter(d -> d.id.equals(docId))
                .findFirst()
                .map(d -> String.format("标题:%s\n分类:%s\n内容:%s", d.title, d.category, d.content))
                .orElse("文档不存在:#" + docId);
    }

    // ========== Tool: 推荐相关文档 ==========

    @ToolMapping(description = "根据分类推荐相关文档")
    public String recommendDocs(
            @Param(description = "文档分类,如:入门、核心、AI、Cloud、Flow、Data、Security") String category
    ) {
        List<DocEntry> results = DOCS.stream()
                .filter(d -> d.category.equals(category))
                .collect(Collectors.toList());

        if (results.isEmpty()) {
            return "分类「" + category + "」下暂无文档";
        }

        StringBuilder sb = new StringBuilder("推荐文档:\n");
        for (DocEntry doc : results) {
            sb.append(String.format("- [%s] %s\n", doc.id, doc.title));
        }
        return sb.toString();
    }

    // ========== Resource: 知识库目录 ==========

    @ResourceMapping(uri = "knowledge://catalog")
    public String getCatalog() {
        StringBuilder sb = new StringBuilder("# 企业知识库目录\n\n");
        for (String item : CATALOG) {
            sb.append(item).append("\n");
        }
        return sb.toString();
    }

    // ========== Prompt: 专业问答模板 ==========

    @PromptMapping(name = "expert_qa")
    public String expertQaPrompt(
            @Param(description = "问题领域") String domain,
            @Param(description = "用户问题") String question
    ) {
        return String.format("""
        你是一位资深的技术专家,专注于 %s 领域。
        
        请基于知识库中的内容,回答以下问题:
        %s
        
        要求:
        1. 先引用相关的知识库文档
        2. 给出清晰的技术解答
        3. 如有代码示例,请确保可直接运行
        """, domain, question);
    }

    // ========== 数据模型 ==========

    static class DocEntry {
        String id;
        String title;
        String category;
        String content;

        DocEntry(String id, String title, String category, String content) {
            this.id = id;
            this.title = title;
            this.category = category;
            this.content = content;
        }
    }
}

public class KnowledgeApp {
    public static void main(String[] args) {
        Solon.start(KnowledgeApp.class, args);
        System.out.println("知识库 MCP 服务已启动:http://localhost:8080/mcp");
    }
}

5.3 客户端调用

java 复制代码
package com.example.knowledge;

import org.noear.solon.ai.mcp.McpChannel;
import org.noear.solon.ai.mcp.client.McpClientProvider;
import org.noear.solon.ai.ChatModel;

import java.util.Map;

public class KnowledgeClient {
    public static void main(String[] args) {
        // 构建 MCP 客户端
        McpClientProvider mcpClient = McpClientProvider.builder()
                .channel(McpChannel.STREAMABLE)
                .url("http://localhost:8080/mcp")
                .build();

        // 方式1:直接调用工具(测试)
        String result = mcpClient.callTool("searchDocs", Map.of("keyword", "AI", "limit", 3))
                .getContent();
        System.out.println("搜索结果:\n" + result);

        // 方式2:集成到大模型(生产)
        ChatModel chatModel = ChatModel.of("http://127.0.0.1:11434/api/chat")
                .provider("ollama")
                .model("qwen2.5:7b")
                .defaultToolsAdd(mcpClient)
                .build();

        String answer = chatModel.prompt("帮我查一下 Solon AI 开发相关的文档")
                .call()
                .getMessage()
                .getContent();

        System.out.println("AI 回答:\n" + answer);
    }
}

六、高级特性

6.1 服务端异步返回

支持异步返回(org.reactivestreams.Publisher):

java 复制代码
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;

@ToolMapping(description = "异步查询大数据")
public Publisher<String> asyncQuery(@Param(description = "查询条件") String query) {
    return Flux.create(emitter->{
        emitter.next("正在查询...");
        emitter.next("查询完成,共 100 条记录");
        emitter.complete();
    });
}

6.2 Web API 与 MCP 互通

一个类同时提供 Web API 和 MCP 服务:

java 复制代码
@Controller
@McpServerEndpoint(channel = McpChannel.STREAMABLE, mcpEndpoint = "/mcp")
public class UnifiedApi {

    @Mapping("/api/weather")
    @ToolMapping(description = "查询天气预报")
    public String getWeather(@Param(description = "城市") String city) {
        return "晴,14°C";
    }
}

同一个方法,Web 通过 /api/weather 访问,MCP 通过工具发现自动调用。

6.3 第三方框架嵌入

Solon AI MCP 可以嵌入到任何 Java 框架中:

xml 复制代码
<!-- SpringBoot 项目 -->
<dependency>
    <groupId>org.noear</groupId>
    <artifactId>solon-ai-mcp</artifactId>
</dependency>

官方提供完整嵌入示例:

七、调试技巧

7.1 快速验证工具

java 复制代码
// 列出所有可用工具
mcpClient.getTools().forEach(tool -> {
    System.out.println(tool.name() + ": " + tool.description());
});

// 直接调用测试
String result = mcpClient.callTool("hello", Map.of("name", "测试")).getContent();

7.2 读取资源

java 复制代码
// 读取文本资源
String catalog = mcpClient.readResource("knowledge://catalog").getContent();
System.out.println(catalog);

7.3 获取提示模板

java 复制代码
// 获取提示模板内容
String prompt = mcpClient.getPrompt("expert_qa").getUserContent();
System.out.println(prompt);

总结

能力 说明
服务端 @McpServerEndpoint + @ToolMapping / @ResourceMapping / @PromptMapping
客户端 McpClientProvider.builder() 构建连接
三种原语 Tool(工具)、Resource(资源)、Prompt(提示模板)
四种通道 Streamable / Streamable Stateless / SSE / STDIO
模型集成 chatModel.defaultToolsAdd(mcpClient) 一行接入
框架嵌入 支持 SpringBoot、jFinal、Vert.X 等任意 Java 框架
Java 兼容 Java 8 ~ Java 26

3 行代码启动 MCP 服务,1 行代码接入大模型 ------ 这就是 Solon AI MCP 的开发体验。

相关推荐
砍材农夫8 小时前
物联网 基于netty构建mqtt协议规范(主题通配符订阅)
java·前端·javascript·物联网·netty
_日拱一卒8 小时前
LeetCode:114二叉树展开为链表
java·开发语言·算法
SamDeepThinking8 小时前
面试官问Bean线程安全,你该从架构角度回答
java·后端·面试
敖正炀8 小时前
ArrayList 与 LinkedList 源码全景:从数据结构选择到性能分歧的完整代码路径
java
凌波粒8 小时前
LeetCode--513.找树左下角的值(二叉树)
java·算法·leetcode
敖正炀8 小时前
HashMap 红黑树化与退化
java
喜欢小苹果的码农8 小时前
xxl-job主流程分析
java
Honey Ro8 小时前
浅析大模型 Agent 的记忆(Memory)机制
深度学习·语言模型·llm·rag
敖正炀8 小时前
HashMap 源码深度拆解(JDK 7→8)
java