前面的 MCP Server 使用 Python 编写,但 MCP 本身与语言无关。只要客户端和服务端遵守同一协议,Python Agent 就可以调用 Java Tool。
这一篇使用 Spring Boot 3.5.3 和 Spring AI 1.1.5 编写 Java Streamable HTTP MCP Server,并进行三层验证:
- JUnit 验证 Java 业务函数。
- Python MCP Client 直接调用 Java Tool。
- 本地 Qwen3 Agent 通过 MCP Adapter 调用 Java Tool。
这样可以分别判断业务逻辑、MCP 服务和 Agent 工具选择是否正确。
1. 完整调用链

最终链路如下:
用户问题
→ 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。

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,失败时很难判断问题位于哪里。

- 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 的内部实现。