构建自定义MCP Server:把你的企业API暴露给AI
构建自定义MCP Server:把你的企业API暴露给AI
开篇故事:赵磊的CRM集成困境
赵磊是一家SaaS公司的高级Java工程师,工作了4年。2025年底,公司决定引入AI助手,让销售团队能用自然语言查询CRM数据。
需求听起来很简单:销售说"帮我查一下张三公司最近3个月的订单",AI返回结果。
但赵磊实现起来非常痛苦。
第一周,他用Function Calling方案。为每个查询写一个工具函数,定义JSON Schema,处理参数校验,写了大概800行代码。然后销售说:"能不能也查合同?能不能查客户联系人?"又得重复整套流程。
第二周,他发现每次接入新AI模型(公司在Claude和GPT-4之间纠结),Function Calling的格式不一样,代码要改两遍。
第三周,他在GitHub上刷到了MCP(Model Context Protocol)。
MCP是Anthropic在2024年11月提出的开放协议,核心思想是:把工具和数据资源的定义标准化,让任何AI模型都能通过统一协议调用。就像HTTP统一了Web通信,MCP统一了AI与工具的通信。
赵磊花了3天,把CRM的查询接口包装成MCP Server。之后:
- 新增查询功能:只需加一个
@McpTool注解,15分钟搞定 - 切换AI模型:MCP Server完全不用改
- 其他项目组想用CRM数据:直接复用同一个MCP Server
原来需要800行的Function Calling代码,现在整个MCP Server核心逻辑不到300行。
这篇文章,我带你从零实现一个生产级CRM查询MCP Server。
一、MCP协议架构深度解析
1.1 MCP的核心概念
MCP定义了三类核心能力:
┌─────────────────────────────────────────┐
│ MCP Server能力 │
├─────────────┬─────────────┬─────────────┤
│ Tools │ Resources │ Prompts │
│ (工具) │ (资源) │ (提示模板) │
│ AI可以调用 │ AI可以读取 │ AI可以使用 │
│ 的函数 │ 的数据 │ 的预设提示 │
└─────────────┴─────────────┴─────────────┘Tools(工具):AI可以主动调用的函数,有明确的输入/输出,例如"查询客户信息"。
Resources(资源):AI可以读取的数据,例如"客户列表"、"产品手册"。
Prompts(提示模板):预定义的提示词模板,帮助AI完成特定任务。
1.2 MCP通信架构
1.3 MCP传输层
MCP支持两种传输方式:
| 传输方式 | 适用场景 | 优缺点 |
|---|---|---|
| stdio | 本地工具、CLI集成 | 简单,但只能本地访问 |
| HTTP/SSE | 远程服务、企业部署 | 支持网络访问,可认证 |
企业级部署必须用HTTP/SSE方式,本文重点讲这个。
1.4 整体系统架构
二、Spring Boot MCP Server依赖与配置
2.1 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.3.5</version>
<relativePath/>
</parent>
<groupId>com.company</groupId>
<artifactId>crm-mcp-server</artifactId>
<version>1.0.0</version>
<name>CRM MCP Server</name>
<properties>
<java.version>21</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
</properties>
<dependencies>
<!-- Spring Boot核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI MCP Server核心依赖 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- Spring AI MCP WebFlux传输(支持SSE) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-webflux-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- 数据库访问 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<!-- Redis缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- JSON处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Lombok减少样板代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>2.2 application.yml完整配置
spring:
application:
name: crm-mcp-server
# 数据源配置
datasource:
url: jdbc:mysql://localhost:3306/crm_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
username: ${DB_USERNAME:root}
password: ${DB_PASSWORD:password}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
minimum-idle: 5
maximum-pool-size: 20
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
pool-name: CrmHikariPool
# JPA配置
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.MySQLDialect
format_sql: true
# Redis配置
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
timeout: 3000ms
lettuce:
pool:
max-active: 10
max-idle: 5
min-idle: 1
# MCP Server配置
ai:
mcp:
server:
name: crm-mcp-server
version: 1.0.0
# 传输类型:sse(Server-Sent Events)适合生产环境
transport: sse
# SSE端点路径
sse-message-endpoint: /mcp/message
# 工具变更通知
tool-change-notification: true
# 资源变更通知
resource-change-notification: true
# 服务器配置
server:
port: 8080
servlet:
context-path: /
# MCP认证配置(自定义)
mcp:
security:
api-key-header: X-MCP-API-Key
enabled: true
# API Keys列表(生产环境应从数据库或Vault读取)
valid-keys:
- key: "mcp-key-prod-abc123"
client: "claude-desktop"
permissions: [READ]
- key: "mcp-key-dev-xyz789"
client: "dev-testing"
permissions: [READ, WRITE]
# 工具配置
tools:
cache:
enabled: true
ttl: 300 # 秒
rate-limit:
enabled: true
requests-per-minute: 100
# 日志配置
logging:
level:
com.company.crm: DEBUG
org.springframework.ai.mcp: INFO
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
# Actuator配置
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always2.3 主启动类
package com.company.crm;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableAsync;
/**
* CRM MCP Server启动类
* 通过MCP协议向AI暴露CRM系统的查询能力
*/
@SpringBootApplication
@EnableCaching
@EnableAsync
public class CrmMcpServerApplication {
public static void main(String[] args) {
SpringApplication.run(CrmMcpServerApplication.class, args);
}
}三、实现MCP Tool:@McpTool注解定义工具
3.1 MCP Tool的设计原则
每个Tool应该:
- 单一职责:一个Tool只做一件事
- 描述清晰:AI根据description决定何时调用,描述必须准确
- 错误友好:返回AI能理解的错误信息,而不是技术异常
3.2 客户查询工具实现
package com.company.crm.tool;
import com.company.crm.dto.CustomerDTO;
import com.company.crm.service.CustomerService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 客户查询工具集
* 向AI暴露CRM客户相关查询能力
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CustomerQueryTools {
private final CustomerService customerService;
/**
* 根据公司名称查询客户基本信息
*/
@Tool(description = """
根据公司名称查询客户的基本信息,包括:公司全称、行业、规模、联系人、
联系电话、邮箱、地址、客户等级、负责销售、创建时间等。
当用户询问某家公司的客户信息时使用此工具。
支持模糊搜索,例如输入"张三"可以匹配"张三科技有限公司"。
""")
public CustomerDTO getCustomerByCompanyName(
@ToolParam(description = "公司名称,支持模糊匹配,例如:'张三科技'") String companyName) {
log.info("MCP Tool调用 - getCustomerByCompanyName: {}", companyName);
CustomerDTO customer = customerService.findByCompanyNameFuzzy(companyName);
if (customer == null) {
// 返回AI友好的错误信息,而不是抛出异常
return CustomerDTO.notFound("未找到名称包含 '" + companyName + "' 的客户,请确认公司名称是否正确");
}
return customer;
}
/**
* 根据客户ID查询详细信息
*/
@Tool(description = """
根据客户ID查询客户详细信息。
当已知客户ID时使用此工具获取完整客户信息。
客户ID通常是形如 'C2024001' 的编号。
""")
public CustomerDTO getCustomerById(
@ToolParam(description = "客户ID,例如:C2024001") String customerId) {
log.info("MCP Tool调用 - getCustomerById: {}", customerId);
return customerService.findById(customerId)
.orElse(CustomerDTO.notFound("客户ID " + customerId + " 不存在"));
}
/**
* 批量查询某销售负责的客户列表
*/
@Tool(description = """
查询某位销售人员负责的所有客户列表。
返回客户摘要信息(ID、公司名、等级、最近联系时间)。
当需要了解某销售的客户全景时使用。
""")
public List<CustomerDTO> getCustomersBySalesRep(
@ToolParam(description = "销售人员姓名") String salesRepName,
@ToolParam(description = "客户等级过滤,可选值:A/B/C/D/ALL,默认ALL") String grade) {
log.info("MCP Tool调用 - getCustomersBySalesRep: salesRep={}, grade={}", salesRepName, grade);
return customerService.findBySalesRepAndGrade(salesRepName,
"ALL".equals(grade) ? null : grade);
}
/**
* 搜索高价值客户
*/
@Tool(description = """
搜索高价值客户(A级和B级客户)。
可以按行业、地区、规模等条件过滤。
用于市场分析、客户盘点等场景。
""")
public List<CustomerDTO> searchHighValueCustomers(
@ToolParam(description = "行业,例如:'互联网'、'金融'、'制造业',不限制传null") String industry,
@ToolParam(description = "省份,例如:'广东'、'北京',不限制传null") String province,
@ToolParam(description = "返回数量限制,最大100,默认20") int limit) {
log.info("MCP Tool调用 - searchHighValueCustomers: industry={}, province={}, limit={}",
industry, province, limit);
int actualLimit = Math.min(limit <= 0 ? 20 : limit, 100);
return customerService.searchHighValueCustomers(industry, province, actualLimit);
}
}3.3 订单查询工具实现
package com.company.crm.tool;
import com.company.crm.dto.OrderDTO;
import com.company.crm.dto.OrderSummaryDTO;
import com.company.crm.service.OrderService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.List;
/**
* 订单查询工具集
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class OrderQueryTools {
private final OrderService orderService;
/**
* 查询客户的订单列表
*/
@Tool(description = """
查询指定客户的订单列表。
可以按时间范围过滤,返回订单号、金额、状态、产品等信息。
当用户询问某客户的订单情况时使用此工具。
时间格式为 yyyy-MM-dd,例如:2024-01-01。
""")
public List<OrderDTO> getOrdersByCustomer(
@ToolParam(description = "客户ID或公司名称(会自动解析)") String customerIdOrName,
@ToolParam(description = "开始日期,格式yyyy-MM-dd,例如:2024-01-01") String startDate,
@ToolParam(description = "结束日期,格式yyyy-MM-dd,例如:2024-12-31") String endDate) {
log.info("MCP Tool调用 - getOrdersByCustomer: customer={}, start={}, end={}",
customerIdOrName, startDate, endDate);
LocalDate start = startDate != null ? LocalDate.parse(startDate) : LocalDate.now().minusMonths(3);
LocalDate end = endDate != null ? LocalDate.parse(endDate) : LocalDate.now();
return orderService.findByCustomerAndDateRange(customerIdOrName, start, end);
}
/**
* 获取订单统计摘要
*/
@Tool(description = """
获取客户的订单统计摘要,包括:订单总数、总金额、平均单价、
最大订单、最近订单时间、按产品分类统计等。
当需要了解客户的整体购买情况时使用此工具。
""")
public OrderSummaryDTO getOrderSummary(
@ToolParam(description = "客户ID或公司名称") String customerIdOrName,
@ToolParam(description = "统计周期(月数),例如:3表示近3个月,12表示近一年") int monthsBack) {
log.info("MCP Tool调用 - getOrderSummary: customer={}, months={}", customerIdOrName, monthsBack);
return orderService.getOrderSummary(customerIdOrName, monthsBack);
}
/**
* 查询待签订单
*/
@Tool(description = """
查询当前处于"待签约"或"谈判中"状态的订单(商机)。
可按销售、金额范围过滤。
用于了解销售管道(Pipeline)情况。
""")
public List<OrderDTO> getPendingOrders(
@ToolParam(description = "销售人员姓名,不限制传null") String salesRepName,
@ToolParam(description = "最小金额(万元),例如:10 表示10万以上") double minAmount) {
log.info("MCP Tool调用 - getPendingOrders: salesRep={}, minAmount={}", salesRepName, minAmount);
return orderService.findPendingOrders(salesRepName, minAmount * 10000);
}
}四、实现MCP Resource:向AI暴露数据资源
Resource与Tool的区别:
- Tool:AI主动调用,执行动作,有明确输入参数
- Resource:AI读取数据,类似文件或数据库表,URI定位
package com.company.crm.resource;
import com.company.crm.service.CustomerService;
import com.company.crm.service.ProductService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.mcp.server.annotation.McpResource;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* MCP资源提供者
* 向AI暴露只读数据资源
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CrmResourceProvider {
private final CustomerService customerService;
private final ProductService productService;
private final ObjectMapper objectMapper;
/**
* 客户等级定义资源
* AI可以读取这个资源了解客户等级的含义
*/
@McpResource(
uri = "crm://config/customer-grades",
name = "客户等级定义",
description = "CRM系统中客户等级(A/B/C/D)的定义和标准,AI在解释客户等级时应参考此资源",
mimeType = "application/json"
)
public String getCustomerGradeDefinitions() {
Map<String, Object> grades = Map.of(
"A", Map.of(
"name", "战略客户",
"criteria", "年合同金额>100万或战略合作关系",
"serviceLevel", "专属客户经理,24小时响应"
),
"B", Map.of(
"name", "重要客户",
"criteria", "年合同金额20-100万",
"serviceLevel", "优先响应,4小时内回复"
),
"C", Map.of(
"name", "普通客户",
"criteria", "年合同金额5-20万",
"serviceLevel", "标准服务,1个工作日响应"
),
"D", Map.of(
"name", "潜在客户",
"criteria", "尚未签约或年合同金额<5万",
"serviceLevel", "自助服务为主"
)
);
try {
return objectMapper.writeValueAsString(grades);
} catch (Exception e) {
return "{}";
}
}
/**
* 产品目录资源
* AI在回答产品相关问题时可以读取
*/
@McpResource(
uri = "crm://catalog/products",
name = "产品目录",
description = "公司所有产品的名称、编号、价格、功能描述列表",
mimeType = "application/json"
)
public String getProductCatalog() {
try {
return objectMapper.writeValueAsString(productService.getAllProducts());
} catch (Exception e) {
log.error("获取产品目录失败", e);
return "[]";
}
}
/**
* 销售团队信息资源
*/
@McpResource(
uri = "crm://team/sales-reps",
name = "销售团队",
description = "销售团队成员列表,包括姓名、工号、负责区域、专属客户数",
mimeType = "application/json"
)
public String getSalesTeam() {
try {
return objectMapper.writeValueAsString(customerService.getSalesTeamSummary());
} catch (Exception e) {
log.error("获取销售团队信息失败", e);
return "[]";
}
}
}五、输入输出Schema定义
5.1 核心DTO类
package com.company.crm.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
* 客户数据传输对象
* MCP工具的输出类型,字段描述直接影响AI的理解
*/
@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CustomerDTO {
private String customerId; // 客户ID,如:C2024001
private String companyName; // 公司全名
private String industry; // 行业
private String scale; // 企业规模:小型/中型/大型
private String grade; // 客户等级:A/B/C/D
private String contactName; // 主要联系人
private String contactPhone; // 联系电话
private String contactEmail; // 联系邮箱
private String address; // 注册地址
private String salesRepName; // 负责销售姓名
private LocalDate lastContactDate; // 最近联系日期
private BigDecimal totalRevenue; // 历史累计金额(元)
private String status; // 客户状态:active/inactive/churned
private LocalDateTime createdAt; // 创建时间
// 错误信息(工具找不到数据时使用)
private String errorMessage;
private boolean found = true;
/**
* 工厂方法:创建未找到的响应
*/
public static CustomerDTO notFound(String message) {
return CustomerDTO.builder()
.found(false)
.errorMessage(message)
.build();
}
}package com.company.crm.dto;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
/**
* 订单统计摘要DTO
*/
@Data
@Builder
public class OrderSummaryDTO {
private String customerId;
private String companyName;
private int totalOrders; // 订单总数
private BigDecimal totalAmount; // 总金额(元)
private BigDecimal averageOrderAmount; // 平均单价(元)
private BigDecimal maxOrderAmount; // 最大订单金额(元)
private LocalDate firstOrderDate; // 首次下单日期
private LocalDate lastOrderDate; // 最近下单日期
private Map<String, BigDecimal> amountByProduct; // 按产品分类的金额
private Map<String, Integer> countByStatus; // 按状态分类的数量
private String statisticsPeriod; // 统计周期描述,如"近3个月"
}package com.company.crm.dto;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 订单DTO
*/
@Data
@Builder
public class OrderDTO {
private String orderId; // 订单号
private String customerId; // 客户ID
private String companyName; // 公司名称
private String productName; // 产品名称
private BigDecimal amount; // 订单金额(元)
private String status; // 状态:pending/signed/delivered/cancelled
private String statusLabel; // 状态中文描述
private LocalDate orderDate; // 下单日期
private LocalDate expectedDate; // 预计交付日期
private String salesRepName; // 销售人员
private String remark; // 备注
}六、合同查询工具实现
package com.company.crm.tool;
import com.company.crm.dto.ContractDTO;
import com.company.crm.service.ContractService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 合同查询工具集
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ContractQueryTools {
private final ContractService contractService;
@Tool(description = """
查询指定客户的合同列表。
返回合同编号、合同名称、金额、签约日期、有效期、状态等信息。
合同状态包括:草稿(draft)、审批中(reviewing)、已签约(signed)、
执行中(executing)、已完成(completed)、已终止(terminated)。
""")
public List<ContractDTO> getContractsByCustomer(
@ToolParam(description = "客户ID或公司名称") String customerIdOrName,
@ToolParam(description = "合同状态过滤,传null查询全部") String status) {
log.info("MCP Tool调用 - getContractsByCustomer: customer={}, status={}",
customerIdOrName, status);
return contractService.findByCustomer(customerIdOrName, status);
}
@Tool(description = """
查询即将到期的合同(默认30天内)。
用于提醒销售及时跟进续签。
返回合同列表,按到期时间升序排列。
""")
public List<ContractDTO> getExpiringContracts(
@ToolParam(description = "到期天数,例如30表示30天内到期,默认30") int daysToExpire,
@ToolParam(description = "销售人员姓名,不限制传null") String salesRepName) {
log.info("MCP Tool调用 - getExpiringContracts: days={}, salesRep={}", daysToExpire, salesRepName);
int days = daysToExpire <= 0 ? 30 : daysToExpire;
return contractService.findExpiringContracts(days, salesRepName);
}
@Tool(description = """
获取合同详细信息,包括合同条款摘要、付款计划、关联订单等。
当需要了解合同具体内容时使用。
""")
public ContractDTO getContractDetail(
@ToolParam(description = "合同编号,例如:CT2024-001") String contractId) {
log.info("MCP Tool调用 - getContractDetail: contractId={}", contractId);
return contractService.findById(contractId)
.orElse(ContractDTO.notFound("合同 " + contractId + " 不存在"));
}
}七、MCP Server的API Key认证
7.1 认证配置类
package com.company.crm.security;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* MCP认证配置
*/
@Data
@Component
@ConfigurationProperties(prefix = "mcp.security")
public class McpSecurityProperties {
private boolean enabled = true;
private String apiKeyHeader = "X-MCP-API-Key";
private List<ApiKeyConfig> validKeys;
@Data
public static class ApiKeyConfig {
private String key;
private String client;
private List<String> permissions;
}
}7.2 认证过滤器
package com.company.crm.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* MCP API Key认证过滤器
* 所有MCP请求必须携带有效的API Key
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class McpAuthFilter extends OncePerRequestFilter {
private final McpSecurityProperties securityProperties;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 只拦截MCP相关路径
String path = request.getRequestURI();
if (!path.startsWith("/mcp")) {
filterChain.doFilter(request, response);
return;
}
// 认证功能未开启
if (!securityProperties.isEnabled()) {
filterChain.doFilter(request, response);
return;
}
// 获取API Key
String apiKey = request.getHeader(securityProperties.getApiKeyHeader());
if (apiKey == null || apiKey.isBlank()) {
log.warn("MCP请求缺少API Key: {}", path);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("""
{"error": "Missing API Key", "header": "%s"}
""".formatted(securityProperties.getApiKeyHeader()));
return;
}
// 验证API Key
boolean valid = securityProperties.getValidKeys().stream()
.anyMatch(k -> k.getKey().equals(apiKey));
if (!valid) {
log.warn("无效的MCP API Key: {}...", apiKey.substring(0, Math.min(8, apiKey.length())));
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json");
response.getWriter().write("""
{"error": "Invalid API Key"}
""");
return;
}
// 记录调用日志
String clientName = securityProperties.getValidKeys().stream()
.filter(k -> k.getKey().equals(apiKey))
.map(McpSecurityProperties.ApiKeyConfig::getClient)
.findFirst()
.orElse("unknown");
log.info("MCP请求通过认证 - client: {}, path: {}", clientName, path);
// 将客户端信息存入请求属性
request.setAttribute("mcp.client", clientName);
filterChain.doFilter(request, response);
}
}7.3 安全配置
package com.company.crm.security;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security配置
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final McpAuthFilter mcpAuthFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
// 健康检查和监控端点不需要认证
.requestMatchers("/actuator/health", "/actuator/info").permitAll()
// 其他所有请求需要认证(由McpAuthFilter处理)
.anyRequest().permitAll()
)
.addFilterBefore(mcpAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}八、工具注册与Spring配置
package com.company.crm.config;
import com.company.crm.tool.ContractQueryTools;
import com.company.crm.tool.CustomerQueryTools;
import com.company.crm.tool.OrderQueryTools;
import org.springframework.ai.mcp.server.McpServer;
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;
/**
* MCP Server配置
* 注册所有Tool和Resource
*/
@Configuration
public class McpServerConfig {
/**
* 注册所有MCP工具
* Spring AI会自动扫描@Tool注解并生成Schema
*/
@Bean
public ToolCallbackProvider crmToolCallbackProvider(
CustomerQueryTools customerTools,
OrderQueryTools orderTools,
ContractQueryTools contractTools) {
return MethodToolCallbackProvider.builder()
.toolObjects(customerTools, orderTools, contractTools)
.build();
}
}九、部署:MCP Server的容器化
9.1 Dockerfile
FROM eclipse-temurin:21-jre-alpine AS runtime
WORKDIR /app
# 创建非root用户运行
RUN addgroup -S mcpgroup && adduser -S mcpuser -G mcpgroup
# 复制jar包
COPY target/crm-mcp-server-1.0.0.jar app.jar
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD wget -q -O- http://localhost:8080/actuator/health || exit 1
# 切换到非root用户
USER mcpuser
# JVM调优参数
ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]9.2 docker-compose.yml
version: '3.8'
services:
crm-mcp-server:
build: .
image: crm-mcp-server:1.0.0
container_name: crm-mcp-server
ports:
- "8080:8080"
environment:
- DB_USERNAME=${DB_USERNAME}
- DB_PASSWORD=${DB_PASSWORD}
- REDIS_HOST=redis
- REDIS_PASSWORD=${REDIS_PASSWORD}
- SPRING_PROFILES_ACTIVE=prod
depends_on:
redis:
condition: service_healthy
restart: unless-stopped
networks:
- mcp-network
volumes:
- ./logs:/app/logs
redis:
image: redis:7-alpine
container_name: mcp-redis
command: redis-server --requirepass ${REDIS_PASSWORD}
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
networks:
- mcp-network
networks:
mcp-network:
driver: bridge十、测试MCP Server
10.1 单元测试
package com.company.crm.tool;
import com.company.crm.dto.CustomerDTO;
import com.company.crm.service.CustomerService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CustomerQueryToolsTest {
@Mock
private CustomerService customerService;
@InjectMocks
private CustomerQueryTools customerQueryTools;
@Test
void getCustomerByCompanyName_found_returnsCustomer() {
// Given
String companyName = "张三科技";
CustomerDTO expected = CustomerDTO.builder()
.customerId("C2024001")
.companyName("张三科技有限公司")
.grade("A")
.build();
when(customerService.findByCompanyNameFuzzy(companyName)).thenReturn(expected);
// When
CustomerDTO result = customerQueryTools.getCustomerByCompanyName(companyName);
// Then
assertThat(result.isFound()).isTrue();
assertThat(result.getCustomerId()).isEqualTo("C2024001");
assertThat(result.getGrade()).isEqualTo("A");
}
@Test
void getCustomerByCompanyName_notFound_returnsErrorMessage() {
// Given
when(customerService.findByCompanyNameFuzzy("不存在公司")).thenReturn(null);
// When
CustomerDTO result = customerQueryTools.getCustomerByCompanyName("不存在公司");
// Then
assertThat(result.isFound()).isFalse();
assertThat(result.getErrorMessage()).contains("不存在公司");
}
}10.2 集成测试(使用MCP Client)
package com.company.crm;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class McpServerIntegrationTest {
@LocalServerPort
private int port;
private McpSyncClient mcpClient;
@BeforeEach
void setUp() {
// 构建MCP客户端连接到测试服务器
mcpClient = McpClient.sync(
HttpClientSseClientTransport.builder("http://localhost:" + port)
.customizeRequest(builder ->
builder.header("X-MCP-API-Key", "mcp-key-dev-xyz789"))
.build()
).build();
mcpClient.initialize();
}
@Test
void listTools_returnsAllRegisteredTools() {
// When
McpSchema.ListToolsResult result = mcpClient.listTools();
// Then
List<McpSchema.Tool> tools = result.tools();
assertThat(tools).isNotEmpty();
List<String> toolNames = tools.stream()
.map(McpSchema.Tool::name)
.toList();
assertThat(toolNames).contains(
"getCustomerByCompanyName",
"getOrdersByCustomer",
"getContractsByCustomer"
);
}
@Test
void callTool_getCustomerByCompanyName_returnsResult() {
// When
McpSchema.CallToolResult result = mcpClient.callTool(
new McpSchema.CallToolRequest(
"getCustomerByCompanyName",
Map.of("companyName", "测试公司")
)
);
// Then
assertThat(result.content()).isNotEmpty();
assertThat(result.isError()).isFalse();
}
}10.3 用curl手动测试
# 1. 测试服务健康状态
curl http://localhost:8080/actuator/health
# 2. 获取工具列表(SSE方式)
curl -H "X-MCP-API-Key: mcp-key-dev-xyz789" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
http://localhost:8080/mcp/message
# 3. 调用工具
curl -H "X-MCP-API-Key: mcp-key-dev-xyz789" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "getCustomerByCompanyName",
"arguments": {"companyName": "张三科技"}
}
}' \
http://localhost:8080/mcp/message十一、性能数据与生产优化
11.1 实测性能数据
在标准配置(4核8G,MySQL + Redis)下:
| 操作类型 | 平均响应时间 | P99响应时间 | QPS |
|---|---|---|---|
| 客户查询(命中缓存) | 8ms | 25ms | 1200 |
| 客户查询(未命中缓存) | 45ms | 120ms | 280 |
| 订单列表查询 | 65ms | 180ms | 150 |
| 订单统计摘要 | 120ms | 350ms | 80 |
| 合同查询 | 55ms | 160ms | 180 |
11.2 Redis缓存优化
package com.company.crm.service;
import com.company.crm.dto.CustomerDTO;
import com.company.crm.repository.CustomerRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CustomerService {
private final CustomerRepository customerRepository;
/**
* 客户查询结果缓存5分钟
* 键包含查询参数,确保不同查询使用不同缓存
*/
@Cacheable(value = "customer", key = "#companyName", unless = "#result == null")
public CustomerDTO findByCompanyNameFuzzy(String companyName) {
return customerRepository.findByCompanyNameContaining(companyName)
.stream()
.findFirst()
.map(this::toDTO)
.orElse(null);
}
/**
* 当客户数据更新时清除相关缓存
*/
@CacheEvict(value = "customer", allEntries = true)
public void clearCustomerCache() {
// 定时任务调用,每5分钟清除一次缓存
}
private CustomerDTO toDTO(/* Customer entity */ Object entity) {
// 实体转DTO逻辑
return CustomerDTO.builder().build();
}
}十二、与Claude Desktop集成配置
在Claude Desktop的claude_desktop_config.json中添加:
{
"mcpServers": {
"crm": {
"command": "curl",
"args": [
"--no-buffer",
"-H", "X-MCP-API-Key: mcp-key-prod-abc123",
"-H", "Accept: text/event-stream",
"http://your-server:8080/mcp/sse"
]
}
}
}FAQ
Q1:MCP Server和Function Calling有什么区别?
Function Calling是特定AI模型的能力,每个模型格式不同(OpenAI和Anthropic的JSON Schema有细微差异)。MCP是开放协议,写一次,所有支持MCP的AI都能用。另外MCP支持资源(Resources),而Function Calling只有工具(Tools)。
Q2:MCP Server能用在生产环境吗?
Spring AI 1.0.0已经是GA版本,MCP协议规范也已稳定(v0.5+)。本文的方案已有团队在生产使用。关键是做好认证(API Key)、速率限制和审计日志。
Q3:Tool的description怎么写效果最好?
核心原则:描述何时调用,而不仅仅是能做什么。差的描述:"查询客户信息"。好的描述:"当用户询问某家公司的客户详情时使用,支持模糊匹配,例如输入'张三'可以匹配'张三科技有限公司'"。
Q4:多个AI同时调用MCP Server会有并发问题吗?
不会。MCP Server是无状态的,每次请求独立处理。数据库连接池(HikariCP)控制并发数据库连接。Redis缓存减少数据库压力。水平扩展只需起多个实例,前面加个Nginx即可。
Q5:如何调试MCP Server收到了哪些调用?
开启org.springframework.ai.mcp: DEBUG日志,所有收到的工具调用和返回结果都会输出。生产环境建议用审计日志(写入数据库),方便追溯。
Q6:Tool返回值太大(例如几千条记录)怎么处理?
MCP协议对返回值大小没有硬限制,但AI的上下文窗口有限制,太大的返回会被截断甚至导致错误。最佳实践:Tool永远加limit参数,默认返回10-20条,最大不超过100条;如果确实需要大数据,提供分页工具。
总结
本文带你实现了一个完整的生产级CRM MCP Server,核心要点:
- MCP解决的核心问题:统一工具协议,一次实现多模型复用
- 三类核心能力:Tools(工具)、Resources(资源)、Prompts(提示模板)
- 实现关键:
@Tool注解 + 清晰的description是AI正确调用的基础 - 生产必备:API Key认证、Redis缓存、审计日志
- 部署方式:Docker容器化 + 健康检查
下一步建议:
- 基于本文代码,尝试把你们系统中最常被查询的3个接口包装成MCP工具
- 在Claude Desktop中配置并测试
- 观察AI如何组合使用多个工具来回答复杂问题
