MCP 系列 4:使用 Spring AI 开发 Java MCP 服务


前面的 MCP Server 使用 Python 编写,但 MCP 本身与语言无关。只要客户端和服务端遵守同一协议,Python Agent 就可以调用 Java Tool。

这一篇使用 Spring Boot 3.5.3 和 Spring AI 1.1.5 编写 Java Streamable HTTP MCP Server,并进行三层验证:

  1. JUnit 验证 Java 业务函数。
  2. Python MCP Client 直接调用 Java Tool。
  3. 本地 Qwen3 Agent 通过 MCP Adapter 调用 Java Tool。

这样可以分别判断业务逻辑、MCP 服务和 Agent 工具选择是否正确。

1. 完整调用链

Spring AI Java MCP Server 调用流程

最终链路如下:

用户问题
→ Python Agent 将消息和 Tool Schema 交给 Qwen3
→ Qwen3 返回包含 tool_calls 的 AIMessage
→ Agent 工具节点调用转换后的 LangChain Tool
→ LangChain MCP Adapter
→ Java Spring AI MCP Server
→ Java @Tool 方法
→ MCP CallToolResult
→ Agent 写入匹配 Tool Call ID 的 ToolMessage
→ Qwen3 根据工具结果生成最终回答

Python 不需要理解 Java 方法怎样实现,Java 也不需要依赖 LangChain。两端通过 MCP Tool Schema 和 Streamable HTTP 通信。

需要注意,Qwen3 不会直接连接 Java Server。第一次模型调用只决定是否使用工具;真正的跨语言调用由 Agent 工具节点和 MCP Adapter 发起。Java 返回结果后,Agent 再把结果写成 ToolMessage,第二次调用 Qwen3 组织最终回答。

2. 环境与版本

本例使用:

Java 17
Maven 3.9.x
Spring Boot 3.5.3
Spring AI 1.1.5

检查本机环境:

java -version
mvn -version

Java 17 是本项目的编译目标。MCP Server 监听 127.0.0.1:18110,不会占用 FastMCP 的 18100。

3. 项目结构

mcp/p04_java_mcp_server/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/cn/hnbian/llmlearning/mcp/
│   │   │   ├── JavaMcpServerApplication.java
│   │   │   ├── LocalTools.java
│   │   │   └── McpToolConfiguration.java
│   │   └── resources/application.properties
│   └── test/java/cn/hnbian/llmlearning/mcp/
│       └── LocalToolsTest.java
├── 01_check_java_mcp.py
├── 02_agent_with_java_mcp.py
├── requirements.txt
└── README.md

4. Maven 依赖

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         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.3</version>
        <relativePath/>
    </parent>

    <groupId>cn.hnbian.llmlearning</groupId>
    <artifactId>java-mcp-server</artifactId>
    <version>1.0.0</version>
    <name>java-mcp-server</name>

    <properties>
        <java.version>17</java.version>
        <spring-ai.version>1.1.5</spring-ai.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>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

关键依赖是:

<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>

它会自动配置 Spring MVC 版本的 MCP Server。使用 Spring AI BOM 管理 Spring AI 组件版本,避免手工给每个相关依赖重复指定版本。

5. 定义 Java Tool

LocalTools.java:

package cn.hnbian.llmlearning.mcp;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;

import java.util.Map;

/** 提供两个不依赖外部网络和系统时间的固定 Java 工具。 */
@Service
public class LocalTools {

    @Tool(description = "执行两个数字的加、减、乘、除运算")
    public double calculate(
            @ToolParam(description = "第一个数字") double a,
            @ToolParam(description = "第二个数字") double b,
            @ToolParam(description = "运算类型:add、subtract、multiply、divide") String operation) {
        return switch (operation) {
            case "add" -> a + b;
            case "subtract" -> a - b;
            case "multiply" -> a * b;
            case "divide" -> {
                if (b == 0) {
                    throw new IllegalArgumentException("除数不能为 0");
                }
                yield a / b;
            }
            default -> throw new IllegalArgumentException("不支持的运算类型:" + operation);
        };
    }

    @Tool(description = "查询固定城市资料")
    public String getCityInfo(
            @ToolParam(description = "城市中文名") String city) {
        Map<String, String> cityData = Map.of(
                "杭州", "杭州是浙江省省会,以西湖闻名。",
                "北京", "北京是中华人民共和国首都。"
        );
        return cityData.getOrDefault(city, "暂时没有 " + city + " 的资料。");
    }
}

未显式指定工具名时,@Tool 使用 Java 方法名,并提供工具说明;@ToolParam 提供参数说明。Spring AI 根据方法签名生成 JSON Schema。

计算器使用固定运算类型,而不是执行模型返回的任意表达式。这比直接把用户字符串交给脚本解释器安全。

6. 注册 ToolCallbackProvider

只有 @Tool 方法还不够,还要把工具回调注册给 MCP Server。

Spring AI MCP Tool 注册流程

ToolCallbacks.from(localTools) 负责把带注解的方法转换成 ToolCallback,ToolCallbackProvider 再把这些回调作为 Spring Bean 提供给 MCP Server 自动配置。最终,客户端才能通过 tools/list 发现 Schema,并通过 tools/call 分派到对应的 Java 方法。

McpToolConfiguration.java:

package cn.hnbian.llmlearning.mcp;

import org.springframework.ai.support.ToolCallbacks;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/** 将带有 @Tool 注解的 Java 方法注册到 MCP Server。 */
@Configuration
public class McpToolConfiguration {

    @Bean
    ToolCallbackProvider localToolCallbacks(LocalTools localTools) {
        return ToolCallbackProvider.from(ToolCallbacks.from(localTools));
    }
}

这里使用 Spring AI 1.1.5 的导入路径:

org.springframework.ai.support.ToolCallbacks

如果从旧示例复制了其他包路径,编译时会出现找不到 ToolCallbacks 的错误。遇到这种问题应以项目锁定版本的 API 为准,不要混用里程碑版本代码。

7. 启动类和配置

JavaMcpServerApplication.java:

package cn.hnbian.llmlearning.mcp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/** 启动 Java Streamable HTTP MCP Server。 */
@SpringBootApplication
public class JavaMcpServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(JavaMcpServerApplication.class, args);
    }
}

application.properties:

spring.application.name=java-learning-mcp-server
server.address=127.0.0.1
server.port=18110

spring.ai.mcp.server.name=java-learning-mcp-server
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.type=SYNC
spring.ai.mcp.server.protocol=STREAMABLE
spring.ai.mcp.server.instructions=提供本地计算和固定城市资料工具

spring.main.banner-mode=off
logging.level.root=INFO

protocol=STREAMABLE 表示使用 Streamable HTTP。默认 MCP 端点为:

http://127.0.0.1:18110/mcp

type=SYNC 表示 Tool Callback 按同步方式执行,与 Python 客户端是否使用 asyncio 是两回事。

8. 先用 JUnit 验证业务逻辑

在验证 MCP 协议前,先确认工具函数本身没有错误。

LocalToolsTest.java:

package cn.hnbian.llmlearning.mcp;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

/** 验证 Java MCP 工具本身的计算与固定资料逻辑。 */
class LocalToolsTest {

    private final LocalTools tools = new LocalTools();

    @Test
    void shouldCalculateMultiplication() {
        assertEquals(126.0, tools.calculate(18, 7, "multiply"));
    }

    @Test
    void shouldRejectDivisionByZero() {
        assertThrows(
                IllegalArgumentException.class,
                () -> tools.calculate(10, 0, "divide")
        );
    }

    @Test
    void shouldReturnHangzhouInformation() {
        assertEquals("杭州是浙江省省会,以西湖闻名。", tools.getCityInfo("杭州"));
    }
}

运行测试和打包:

cd mcp/p04_java_mcp_server
mvn test
mvn package

实测三个测试全部通过,并生成:

target/java-mcp-server-1.0.0.jar

9. 启动 Java MCP Server

开发阶段可以直接运行:

mvn spring-boot:run

也可以运行打包后的 Jar:

java -jar target/java-mcp-server-1.0.0.jar

服务日志中可以看到稳定的注册结果:

Registered tools: 2

启动日志会包含真实运行时间,文章不保留这些动态字段。

10. 使用 Python Client 直接调用 Java Tool

01_check_java_mcp.py:

"""连接 Java Spring AI MCP Server,并直接列出和调用 Java Tool。"""

import asyncio

from fastmcp import Client


JAVA_MCP_URL = "http://127.0.0.1:18110/mcp"


async def main() -> None:
    """确认 Python MCP Client 可以调用 Java MCP Tool。"""

    async with Client(JAVA_MCP_URL, timeout=20) as client:
        tools = await client.list_tools()
        print("Java MCP Tools:", [tool.name for tool in tools])

        calculate_result = await client.call_tool(
            "calculate",
            {"a": 18, "b": 7, "operation": "multiply"},
        )
        print("calculate result:", calculate_result.content[0].text)

        city_result = await client.call_tool("getCityInfo", {"city": "杭州"})
        print("city result:", city_result.content[0].text)


if __name__ == "__main__":
    asyncio.run(main())

从 llm_learning 根目录运行:

source .venv_mcp/bin/activate
python mcp/p04_java_mcp_server/01_check_java_mcp.py

真实输出:

Java MCP Tools: ['getCityInfo', 'calculate']
calculate result: 126.0
city result: "杭州是浙江省省会,以西湖闻名。"

这一步没有使用模型。它只验证 Python MCP Client 和 Java MCP Server 可以完成能力发现和工具调用。

城市结果的外层引号来自 Spring AI 对 Java String 返回值的 JSON 序列化;字符串内容本身仍然是杭州资料。数值返回值则直接显示为 126.0。

11. 让本地 Qwen3 Agent 调用 Java Tool

02_agent_with_java_mcp.py:

"""让本地 Qwen3 Agent 调用 Java Spring AI MCP Tool。"""

import asyncio

import httpx
from langchain.agents import create_agent
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_openai import ChatOpenAI


async def main() -> None:
    """验证 Python Agent、Java MCP Server 和 Qwen3 的完整调用链。"""

    client = MultiServerMCPClient(
        {
            "java": {
                "transport": "http",
                "url": "http://127.0.0.1:18110/mcp",
            }
        }
    )
    tools = await client.get_tools()

    with httpx.Client(trust_env=False) as http_client:
        model = ChatOpenAI(
            model="default_model",
            base_url="http://127.0.0.1:18080/v1",
            api_key="not-needed",
            temperature=0,
            max_tokens=160,
            http_client=http_client,
        )
        agent = create_agent(
            model=model,
            tools=tools,
            system_prompt="必须使用 Java MCP 工具完成计算,然后用中文回答。",
        )
        result = await agent.ainvoke(
            {"messages": [{"role": "user", "content": "请计算 18 乘以 7。"}]}
        )

    for message in result["messages"]:
        for call in getattr(message, "tool_calls", []):
            print("Java Tool:", call["name"])
            print("Arguments:", call["args"])
        if message.type == "tool":
            # 只保留 Java Tool 返回的文本,不打印动态内容块 ID。
            tool_result = (
                message.content[0]["text"]
                if isinstance(message.content, list)
                else message.content
            )
            print("Java Tool result:", tool_result)

    print("Qwen3 answer:", result["messages"][-1].content)


if __name__ == "__main__":
    asyncio.run(main())

真实输出:

Java Tool: calculate
Arguments: {'a': 18, 'b': 7, 'operation': 'multiply'}
Java Tool result: 126.0
Qwen3 answer: 18 乘以 7 的结果是 126。

这里已经完成跨语言调用:Tool 的定义和执行都在 Java 进程中,Agent 和模型调用在 Python 进程中。

MCP Adapter 负责在 MCP 结果与 LangChain Tool 返回内容之间转换;ToolMessage 则由 Agent 的工具执行节点写入消息状态。二者处在相邻环节,但不是同一个动作。

12. 为什么要分三层测试

如果只运行最终 Agent,失败时很难判断问题位于哪里。

Spring AI Java MCP 三层验证

  • JUnit 失败:优先检查 Java 业务逻辑。
  • JUnit 成功、Python Client 失败:检查 Spring AI 配置、MCP 端点和协议。
  • Python Client 成功、Agent 失败:检查 Tool Schema、提示词、Qwen3 工具调用和 Adapter。

这种分层验证比反复修改 Agent 提示词更有效。

13. 常见问题

13.1 ToolCallbacks 无法导入

Spring AI 不同阶段版本的包路径可能变化。本例使用 Spring AI 1.1.5,对应:

import org.springframework.ai.support.ToolCallbacks;

不要把旧里程碑版本代码直接复制到 1.1.5 项目。

13.2 /mcp 在浏览器里显示错误

MCP 端点需要正确的请求方法、Accept Header 和初始化消息。应使用 MCP Client 测试,不应使用浏览器页面判断服务是否正常。

13.3 Python 能列出 Tool,但模型不调用

先检查工具描述和参数说明是否清晰,再检查模型服务是否支持工具调用。可以先用 Python Client 直接 call_tool(),确认问题不在 Java Server。

13.4 协议版本发生回退

本文使用的 Spring AI 1.1.5 内置 MCP SDK 支持 2025-06-18。Python 客户端请求 2025-11-25 时,服务端实测会返回其支持的 2025-06-18,并记录一条版本提示;客户端接受该版本后,能力列表和工具调用仍可正常完成。

这说明双方可以通过初始化阶段协商出可用版本,不代表两端实现了相同的协议修订版。排查跨语言调用时,应同时查看客户端请求版本和服务端返回的协议版本,不能只根据 /mcp 可访问就判断完全兼容。

13.5 除数为 0

业务工具会抛出 IllegalArgumentException。实测中,Java MCP Server 将错误返回给客户端,FastMCP Client 抛出内容为“除数不能为 0”的 ToolError,Java 进程不会退出。真实业务还应设计可读的错误结果和重试策略。

13.6 服务暴露到局域网

示例固定:

server.address=127.0.0.1

生产部署需要认证、HTTPS、权限控制和审计,不能只改成 0.0.0.0 就直接公开。

14. 总结

层次 使用的技术 验证结果
Java 业务逻辑 JUnit 乘法、除零错误、城市资料均通过
Java MCP Server Spring AI Streamable HTTP 注册 2 个 Tool
协议直连 Python FastMCP Client 协商回退到 2025-06-18 后,列表与直接调用成功
Agent 集成 LangChain MCP Adapter MCP Tool 转为 LangChain Tool,返回内容交给 Agent 写入 ToolMessage
模型调用 本地 Qwen3 第一次选择 calculate,第二次根据 126.0 生成中文回答

MCP 的跨语言价值已经得到验证:服务端只需要正确公开协议能力,客户端不必复制 Java 代码或理解 Spring Bean 的内部实现。


文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录