基于Java开发MCP Server
·
1、MCP概述
Agent(智能体):所谓的智能体就是让大模型拥有额外的能力,以大模型作为核心大脑,具备工具调用、长期记忆、任务规划三大核心能力。用户向智能体下发指令后,智能体依托大模型进行逻辑决策,自动生成任务执行计划(Planning),同时读取记忆(Memory)模块中的历史上下文、对话记录与过往任务数据,结合实时需求与历史信息综合处理,完成复杂任务的拆解、调度与闭环执行。
Tool(工具):
- 工具是为智能体提供标准化外部能力调用的独立单元,是大模型与外部系统交互的桥梁。
- 以单一职责为设计原则,每个工具专注完成一项具体操作(如搜索、计算、API 调用、数据查询等)。
- 不具备自主决策能力,仅在被智能体触发时执行预设逻辑。
- 需明确定义输入参数与输出格式,例如:
- 搜索工具:接收查询字符串并返回搜索结果
- 计算工具:接收数学表达式并返回计算结果
- 为智能体的任务计划提供稳定、可预期的执行支撑,弥补大模型无法直接操作外部服务、获取实时数据的短板。
- 智能体通过 Function Call 机制实现对工具的调用与执行。
MCP(Model Context Protocol,模型上下文协议):
- 由 Anthropic 于 2024 年 11 月底 推出的一种开放标准协议。
- 核心目标:统一大型语言模型(LLM)与外部数据源、工具之间的通信方式。
- 支持两种主要通信机制:
- 本地通信(Stdio 模式):通过Stdio传输数据,适用于同一台机器上运行的客户端与服务器之间的通信,无需依赖网络端口,部署简单、安全性高。
- 远程通信(SSE + HTTP 模式):利用 SSE(Server-Sent Events)与 HTTP 结合,实现跨网络的实时数据传输,适用于需要访问远程资源或分布式部署的场景,支持鉴权、跨平台调用与流式交互。

Function Calling、MCP 与 AI Agent 的关系:
- Function Calling:是 AI 模型调用函数的基础机制,是实现工具交互的 “底层能力”。
- MCP:是一套标准化的通信协议,让 AI 模型与 API / 工具的交互更通用、更无缝,解决了不同模型、不同工具之间的兼容性问题。
- AI Agent:是自主运行的智能系统,它会利用 Function Calling 和 MCP 协议,完成任务分析、规划、工具调用与执行,最终实现特定目标。
2、MCP的通信机制

3、使用SpringAI接入MCP
1、项目依赖
<?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.2.5</version>
<relativePath/>
</parent>
<groupId>cn.aopmin</groupId>
<artifactId>mcp-server</artifactId>
<version>1.0-SNAPSHOT</version>
<name>spring ai + mcp 示例项目</name>
<properties>
<java.version>17</java.version>
<!-- 1. 升级版本:使用最新的M7版本 -->
<spring-ai.version>1.0.0-M7</spring-ai.version>
</properties>
<!-- 2. 关键:补全仓库配置 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<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.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.28</version>
</dependency>
</dependencies>
</project>
2、配置
spring:
application:
name: mcp-server
# Ollama 配置
ai:
ollama:
base-url: http://localhost:11434
chat:
options:
model: qwen2.5:7b # 替换为你的模型
temperature: 0.7
# MCP Server 配置
mcp:
server:
enabled: true
name: mcp-server
version: 1.0.0
protocol: STREAMABLE # 支持 STREAMABLE 和 HTTP 两种协议
capabilities:
tools: true
resources: false
prompts: false
streamable-http:
mcp-endpoint: /mcp
# MCP Client 配置
client:
enabled: true
server-url: http://localhost:8086
request-timeout: 30s
server:
port: 8086
logging:
level:
cn.aopmin: DEBUG # 项目包级别可调为 DEBUG
org.springframework.ai: INFO # Spring AI 核心日志
3、编写mcp工具
package cn.aopmin.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* MCP 工具服务
* 通过 @Tool 注解暴露方法,供 AI 模型调用
* Spring AI 会自动生成 JSON Schema 描述参数格式[reference:8]
*/
@Slf4j
@Service
public class McpToolService {
// 模拟数据存储
private final Map<String, Map<String, Object>> orders = new HashMap<>();
public McpToolService() {
// 初始化模拟订单
Map<String, Object> order1 = new HashMap<>();
order1.put("orderId", "ORD-001");
order1.put("status", "已发货");
order1.put("estimatedDelivery", "2026-05-01");
order1.put("items", 3);
orders.put("ORD-001", order1);
Map<String, Object> order2 = new HashMap<>();
order2.put("orderId", "ORD-002");
order2.put("status", "处理中");
order2.put("estimatedDelivery", "2026-05-03");
order2.put("items", 1);
orders.put("ORD-002", order2);
}
/**
* 获取订单状态
* @Tool 注解使该方法成为 MCP 工具
*/
@Tool(description = "根据订单ID查询订单的当前状态和详细信息")
public Map<String, Object> getOrderStatus(@ToolParam(description = "订单唯一标识符,如 ORD-001") String orderId) {
log.info("查询订单状态工具被调用=>,orderId: {}", orderId);
if (orderId == null || orderId.trim().isEmpty()) {
throw new IllegalArgumentException("订单ID不能为空");
}
Map<String, Object> order = orders.get(orderId);
if (order == null) {
return Map.of("error", "未找到订单: " + orderId, "orderId", orderId);
}
Map<String, Object> result = new HashMap<>();
result.put("orderId", orderId);
result.put("status", order.get("status"));
result.put("estimatedDelivery", order.get("estimatedDelivery"));
result.put("items", order.get("items"));
result.put("queryTime", LocalDateTime.now().toString());
return result;
}
/**
* 获取服务器时间
*/
@Tool(description = "获取当前服务器时间和日期")
public Map<String, String> getServerTime() {
log.info("获取服务器时间工具被调用=>");
return Map.of(
"time", LocalDateTime.now().toString(),
"timezone", "Asia/Shanghai"
);
}
/**
* 计算器工具
*/
@Tool(description = "执行基本的数学运算,支持加法、减法、乘法、除法")
public Map<String, Object> calculate(
@ToolParam(description = "第一个数字") double a,
@ToolParam(description = "运算符,可选值: add, subtract, multiply, divide") String operation,
@ToolParam(description = "第二个数字") double b) {
log.info("计算器工具被调用=>,a: {}, operation: {}, b: {}", a, operation, b);
double result;
switch (operation.toLowerCase()) {
case "add":
result = a + b;
break;
case "subtract":
result = a - b;
break;
case "multiply":
result = a * b;
break;
case "divide":
if (b == 0) {
return Map.of("error", "除数不能为零");
}
result = a / b;
break;
default:
return Map.of("error", "不支持的运算符: " + operation);
}
return Map.of(
"expression", a + " " + operation + " " + b,
"result", result
);
}
}
4、注册mcp工具,并绑定chatclient上
package cn.aopmin.config;
import cn.aopmin.service.McpToolService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.List;
/**
* Spring AI 核心配置类
*
* 职责:
* 1. 创建 ChatClient ,用于与 Ollama 模型交互
* 2. 自动发现并注册所有通过 @Tool 注解声明的 MCP 工具
*/
@Slf4j
@Configuration
public class McpConfig {
/**
* 手动注册工具(方式一)
* 将带有 @Tool 注解的服务类注册为 ToolCallbackProvider
*/
@Bean
public ToolCallbackProvider mcpToolProvider(McpToolService mcpToolService) {
return MethodToolCallbackProvider.builder()
.toolObjects(mcpToolService)
.build();
}
/**
* 创建 ChatClient 实例,并绑定所有 MCP 工具
*
* @param ollamaChatModel Spring AI 自动配置的 Ollama 聊天模型
* @param toolCallbackProviders 容器中所有的 ToolCallbackProvider Bean
* @return 配置好的 ChatClient
*/
@Bean
public ChatClient chatClient(OllamaChatModel ollamaChatModel,
List<ToolCallbackProvider> toolCallbackProviders) {
// 从所有 ToolCallbackProvider 中提取工具回调函数(FunctionCallback[])
List<FunctionCallback> functionCallbacks = toolCallbackProviders.stream()
.flatMap(provider -> Arrays.stream(provider.getToolCallbacks())).toList();
if (!functionCallbacks.isEmpty()) {
log.info("✅ 已成功加载 {} 个 MCP 工具:", functionCallbacks.size());
functionCallbacks.forEach(fc ->
log.info(" 🔧 {} - {}", fc.getName(), fc.getDescription())
);
} else {
log.warn("⚠️ 未发现任何 @Tool 注解的方法,AI 将无法调用外部工具。");
}
// 构建 ChatClient,将工具注册进去(转换为数组)
return ChatClient.builder(ollamaChatModel)
.defaultFunctions(functionCallbacks.toArray(new FunctionCallback[0]))
.build();
}
}
5、编写service
package cn.aopmin.service;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChatService {
private final ChatClient chatClient;
public String chat(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call()
.content();
}
public String chatWithSystemPrompt(String userMessage, String systemPrompt) {
return chatClient.prompt()
.system(systemPrompt)
.user(userMessage)
.call()
.content();
}
}
6、编写controller
package cn.aopmin.controller;
import cn.aopmin.service.ChatService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/api/chat")
@CrossOrigin(origins = "*")
@RequiredArgsConstructor
public class ChatController {
private final ChatService chatService;
@PostMapping
public ChatResponse chat(@RequestBody ChatRequest request) {
log.info("chat message=> {}", request.message());
String reply = chatService.chat(request.message());
return new ChatResponse(reply);
}
public record ChatRequest(String message) {}
public record ChatResponse(String reply) {}
}
7、启动类
package cn.aopmin;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;
@Slf4j
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
ConfigurableApplicationContext ctx =SpringApplication.run(McpServerApplication.class, args);
log.info("=== MCP Server 启动成功 ===");
Environment environment=ctx.getBean(Environment.class);
String port=environment.getProperty("server.port");
log.info("服务地址: http://localhost:{}", port);
log.info("测试命令(订单查询):\ncurl -X POST http://localhost:{}/api/chat \\\n -H \"Content-Type: application/json\" \\\n -d '{\"message\":\"查询订单ORD-001的状态\"}'", port);
log.info("测试命令 (查询时间) :\ncurl -X POST http://localhost:{}/api/chat \\\n -H \"Content-Type: application/json\" \\\n -d '{\"message\":\"现在几点?\"}'", port);
log.info("测试命令 (计算器) :\ncurl -X POST http://localhost:{}/api/chat \\\n -H \"Content-Type: application/json\" \\\n -d '{\"message\":\"计算 123 + 456\"}'", port);
}
}
8、创建前端页面 resources/static/index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spring AI + Ollama + MCP 智能聊天助手</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.chat-container {
width: 100%;
max-width: 900px;
height: 90vh;
background: white;
border-radius: 24px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-header {
background: linear-gradient(135deg, #4CAF50, #2196F3);
padding: 20px 24px;
color: white;
}
.chat-header h1 {
font-size: 1.5rem;
margin-bottom: 8px;
}
.status-bar {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.85rem;
opacity: 0.9;
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #4CAF50;
animation: pulse 2s infinite;
}
.status-indicator.thinking {
background-color: #FF9800;
animation: pulse 0.5s infinite;
}
.status-indicator.error {
background-color: #f44336;
animation: none;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #f5f5f5;
}
.message {
margin-bottom: 16px;
display: flex;
align-items: flex-start;
}
.message.user {
justify-content: flex-end;
}
.message.user .message-content {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border-radius: 20px 20px 4px 20px;
}
.message.assistant .message-content {
background: white;
color: #333;
border-radius: 20px 20px 20px 4px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.message.system .message-content {
background: #e3f2fd;
color: #1976d2;
border-radius: 12px;
font-size: 0.9rem;
}
.message-content {
max-width: 80%;
padding: 12px 16px;
line-height: 1.5;
word-wrap: break-word;
}
.chat-input-container {
padding: 20px;
background: white;
border-top: 1px solid #e0e0e0;
}
#message-input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 12px;
font-size: 1rem;
resize: vertical;
font-family: inherit;
transition: border-color 0.3s;
}
#message-input:focus {
outline: none;
border-color: #667eea;
}
.button-group {
display: flex;
gap: 12px;
margin-top: 12px;
}
.btn-primary, .btn-secondary {
padding: 10px 24px;
border: none;
border-radius: 30px;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
flex: 1;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102,126,234,0.4);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: #f0f0f0;
color: #666;
}
.btn-secondary:hover {
background: #e0e0e0;
}
.typing-indicator {
display: inline-flex;
gap: 4px;
padding: 12px 16px;
}
.typing-indicator span {
width: 8px;
height: 8px;
background: #999;
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out both;
}
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
@keyframes typing {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
</style>
</head>
<body>
<div class="chat-container">
<!-- 头部 -->
<div class="chat-header">
<h1>🤖 AI 智能助手</h1>
<div class="status-bar">
<span class="status-indicator" id="status-indicator"></span>
<span id="status-text">已就绪</span>
</div>
</div>
<!-- 聊天消息区域 -->
<div class="chat-messages" id="chat-messages">
<div class="message system">
<div class="message-content">
你好!我是 AI 智能助手,已接入以下工具能力:<br>
📦 订单查询 - 根据订单号查询状态<br>
⏰ 获取服务器时间<br>
🧮 计算器 - 支持数学运算<br>
请输入你的问题开始对话!
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="chat-input-container">
<textarea id="message-input"
placeholder="输入你的问题... (Shift+Enter 换行, Enter 发送)"
rows="3"></textarea>
<div class="button-group">
<button id="send-btn" class="btn-primary">发送 ✨</button>
<button id="clear-btn" class="btn-secondary">清空对话</button>
</div>
</div>
</div>
<script>
// 聊天交互逻辑 - 与后端 /api/chat 接口通信
const API_URL = '/api/chat';
// DOM 元素
const messagesContainer = document.getElementById('chat-messages');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-btn');
const clearButton = document.getElementById('clear-btn');
const statusIndicator = document.getElementById('status-indicator');
const statusText = document.getElementById('status-text');
let isWaitingForResponse = false;
function updateStatus(status, text) {
statusIndicator.className = 'status-indicator';
if (status === 'thinking') {
statusIndicator.classList.add('thinking');
statusText.textContent = text || 'AI 正在思考...';
} else if (status === 'error') {
statusIndicator.classList.add('error');
statusText.textContent = text || '连接错误';
} else {
statusText.textContent = text || '已就绪';
}
}
function addMessage(content, type) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}`;
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
// 支持简单 Markdown 样式
let formattedContent = content;
formattedContent = formattedContent.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
formattedContent = formattedContent.replace(/\n/g, '<br>');
contentDiv.innerHTML = formattedContent;
messageDiv.appendChild(contentDiv);
messagesContainer.appendChild(messageDiv);
// 滚动到底部
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
function addLoadingMessage() {
const loadingDiv = document.createElement('div');
loadingDiv.className = 'message assistant';
loadingDiv.id = 'loading-message';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
loadingDiv.appendChild(contentDiv);
messagesContainer.appendChild(loadingDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
function removeLoadingMessage() {
const loading = document.getElementById('loading-message');
if (loading) loading.remove();
}
async function sendMessage() {
const message = messageInput.value.trim();
if (!message || isWaitingForResponse) return;
// 清空输入框
messageInput.value = '';
// 添加用户消息
addMessage(message, 'user');
// 设置等待状态
isWaitingForResponse = true;
sendButton.disabled = true;
// 添加加载动画
addLoadingMessage();
// 更新状态为思考中
updateStatus('thinking', 'AI 正在调用工具并生成回答...');
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: message })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// 移除加载动画
removeLoadingMessage();
// 添加 AI 回复
addMessage(data.reply, 'assistant');
updateStatus('ready', '已就绪');
} catch (error) {
console.error('Error:', error);
removeLoadingMessage();
addMessage('抱歉,发生了错误: ' + error.message, 'assistant');
updateStatus('error', '连接失败');
// 3秒后恢复状态
setTimeout(() => {
if (statusIndicator.classList.contains('error')) {
updateStatus('ready', '已就绪');
}
}, 3000);
} finally {
isWaitingForResponse = false;
sendButton.disabled = false;
messageInput.focus();
}
}
function clearChat() {
// 保留第一条系统消息
const messages = messagesContainer.querySelectorAll('.message:not(.system)');
messages.forEach(msg => msg.remove());
addMessage('对话已清空,有什么我可以帮你的吗?', 'system');
}
// 事件绑定
sendButton.addEventListener('click', sendMessage);
clearButton.addEventListener('click', clearChat);
// 回车发送(Shift+Enter 换行)
messageInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
</script>
</body>
</html>
9、测试
接口测试:
// 测试用例
curl -X POST http://localhost:8086/api/chat \
-H "Content-Type: application/json" \
-d '{"message":"查询订单ORD-001的状态"}'
// 接口返回结果:
{"reply":"您的订单ORD-001的状态如下:\n\n- 订单包含的商品数量:3件\n- 预计送达时间:2026年5月1日\n- 当前状态:已发货\n\n如果有任何问题或需要进一步的帮助,请随时告诉我!"}%


工具调用流程:

更多推荐

所有评论(0)