摘要: Java开发者专属教程,用Spring Boot写一个MCP Server,加几个注解让Claude直接查数据库,全程一个Demo可跟做。
还在手写SQL查数据?
老板群里@你:"查下上个月Pro用户的复购率。"
你打开IDEA,连数据库,写SQL------JOIN、GROUP BY、HAVING。查完导出Excel,整理格式,发群里。40分钟没了。
如果你能直接说一句话呢?"帮我查上个月Pro用户的复购率。"10秒,结果出来了。
很多人觉得AI只会聊天,干不了实事。但MCP这个协议正在改变这件事------它让AI从嘴炮选手变成了能动手的实习生。而且Java开发者有福了,Spring AI官方已经出了MCP Server的Starter,写几个注解,打包成JAR,Claude就能直接调你的方法。
这篇文章,带你用Spring Boot从零搭一个MCP Server,让Claude直接查你的数据库。全程一个Demo,每一步都有代码。
动手前的5分钟
你需要三样东西:Java 17以上(推荐Java 21)、Maven、Spring Boot 3.3以上。
打开 start.spring.io,创建一个Maven项目,依赖只加一个: Model Context Protocol Server 。下载解压后,打开 [pom.xml](http://pom.xml) ,确认依赖存在:
bash
<dependencyManagement>
<dependencies>
<dependency>
org.springframework.ai
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
org.springframework.ai
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
<dependency>
org.springframework.boot
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
com.h2database
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
spring-ai-starter-mcp-server是MCP的Starter,spring-boot-starter-jdbc用来连数据库,h2是内存数据库,省去安装MySQL的麻烦。
在 [application.properties](http://application.properties) 里加两行:
bash
spring.ai.mcp.server.name=database-server
spring.ai.mcp.server.version=1.0.0
spring.datasource.url=jdbc:h2:mem:demo
spring.datasource.driver-class-name=org.h2.Driver
这就够了,Spring Boot的自动配置会帮你搞定剩下的事。接下来准备Demo数据,新建 [schema.sql](http://schema.sql) ,放src/main/resources/下:
bash
CREATE TABLE IF NOT EXISTS users (
id INT PRIMARY KEY,
name VARCHAR(50),
email VARCHAR(100),
plan VARCHAR(20),
created_at VARCHAR(20)
);
CREATE TABLE IF NOT EXISTS orders (
id INT PRIMARY KEY,
user_id INT,
amount DOUBLE,
status VARCHAR(20),
created_at VARCHAR(20)
);
INSERT INTO users VALUES
(1, '张三', 'zhangsan@
mail.com',
'pro', '2025-01-15'),
(2, '李四', 'lisi@
mail.com',
'free', '2025-03-20'),
(3, '王五', 'wangwu@
mail.com',
'pro', '2025-02-10');
INSERT INTO orders VALUES
(1, 1, 99.0, 'completed', '2025-06-01'),
(2, 1, 199.0, 'completed', '2025-07-15'),
(3, 2, 49.0, 'pending', '2025-08-01'),
(4, 3, 99.0, 'completed', '2025-06-20'),
(5, 3, 299.0, 'refunded', '2025-07-01');
在 [application.properties](http://application.properties) 里加一行让Spring Boot自动执行:
bash
spring.sql.init.mode=always
复制粘贴就行,别跳过。后面所有实操都基于这些数据------两张表,3个用户,5条订单。
写你的第一个MCP Server
先写最简版,一个方法,让你感受MCP是怎么回事。新建 [DatabaseToolsService.java](http://DatabaseToolsService.java) :
bash
import
org.springframework.ai.tool.annotation.Tool;
import
org.springframework.ai.tool.annotation.ToolParam;
import
org.springframework.stereotype.Service;
import
org.springframework.jdbc.core.JdbcTemplate;
@Service
public class DatabaseToolsService {
private final JdbcTemplate jdbc;
public DatabaseToolsService(JdbcTemplate jdbc) {
this.jdbc
= jdbc;
}
@Tool(description = "列出数据库中所有表名。"
+ "当用户想了解数据库结构时使用。")
public String listTables() {
var tables =
jdbc.queryForList(
"SELECT table_name FROM "
+ "
information_schema.tables
"
+ "WHERE table_schema='PUBLIC'",
String.class);
return "数据库中的表: " +
String.join(",
", tables);
}
}
注意那个@Tool注解,这就是核心。Spring AI看到这个注解,会自动把这个方法注册成MCP工具。description里的文字就是AI看到的工具说明,AI靠这段话判断什么时候该调你的方法。描述写得好,AI调用就精准;描述写得差,AI就会乱调。
启动项目:
bash
mvn spring-boot:run
没报错就成功了。想验证工具是否注册成功?用MCP Inspector,另开一个终端:
bash
npx @modelcontextprotocol/inspector \
--command "java" \
--args "-jar target/你的jar包名.jar"
或者直接指向启动类:
bash
npx @modelcontextprotocol/inspector \
--command "mvn" \
--args "spring-boot:run" \
--cwd "你的项目路径"
浏览器打开http://localhost:5173,你会看到可视化界面,里面列出了listTables工具,还能直接点击测试。
看到这里你可能想:就这?别急,接下来注册4个工具,这才是重头戏。
完整版:4个工具全部注册
在 [DatabaseToolsService.java](http://DatabaseToolsService.java) 里补全所有方法:
bash
import
org.springframework.ai.tool.annotation.Tool;
import
org.springframework.ai.tool.annotation.ToolParam;
import
org.springframework.stereotype.Service;
import
org.springframework.jdbc.core.JdbcTemplate;
import
java.util.List;
import
java.util.Map;
@Service
public class DatabaseToolsService {
private final JdbcTemplate jdbc;
public DatabaseToolsService(JdbcTemplate jdbc) {
this.jdbc
= jdbc;
}
@Tool(description = "列出数据库中所有表名。"
+ "当用户想了解数据库有哪些表时使用。")
public String listTables() {
var tables =
jdbc.queryForList(
"SELECT table_name FROM "
+ "
information_schema.tables
"
+ "WHERE table_schema='PUBLIC'",
String.class);
return "数据库中的表: " +
String.join(",
", tables);
}
@Tool(description = "查看指定表的结构,包括字段名和类型。"
+ "当用户想了解某个表的字段信息时使用。")
public String describeTable(
@ToolParam(description = "要查询的表名") String tableName) {
var columns =
jdbc.queryForList(
"SELECT column_name, type_name FROM "
+ "
information_schema.columns
"
+ "WHERE table_schema='PUBLIC' "
+ "AND table_name=?",
String.class,
tableName.toUpperCase());
if (
columns.isEmpty())
{
return "表 " + tableName + " 不存在";
}
StringBuilder sb = new StringBuilder();
sb.append("表
").append(tableName).append(" 结构:\n");
for (var col : columns) {
sb.append("
").append(col).append("\n");
}
return
sb.toString();
}
@Tool(description = "执行SQL查询语句(仅允许SELECT)。"
+ "当用户需要查询数据时使用,"
+ "会返回格式化的查询结果。")
public String executeQuery(
@ToolParam(description = "SELECT查询语句") String sql) {
String upper =
sql.strip().toUpperCase();
if (!
upper.startsWith("SELECT"))
{
return "安全限制:只允许SELECT查询";
}
var dangerous =
List.of(
"DROP", "DELETE", "UPDATE",
"INSERT", "ALTER", "CREATE");
for (var kw : dangerous) {
if (
upper.contains(kw))
{
return "安全限制:检测到危险操作";
}
}
try {
var rows =
jdbc.queryForList(sql);
if (
rows.isEmpty())
{
return "查询结果为空";
}
StringBuilder sb = new StringBuilder();
var keys =
rows.get(0).keySet();
sb.append(String.join("
| ", keys)).append("\n");
sb.append("-".repeat(sb.length()
- 1)).append("\n");
int count = 0;
for (var row : rows) {
if (count++ >= 50) {
sb.append("...
已截断,共")
.append(
rows.size()).append("行");
break;
}
sb.append(String.join("
| ",
keys.stream()
.map(k ->
String.valueOf(row.get(k)))
.toList())).append("\n");
}
return
sb.toString();
} catch (Exception e) {
return "查询错误: " +
e.getMessage();
}
}
@Tool(description = "获取用户摘要信息,"
+ "包括基本信息和订单统计。"
+ "当用户想了解某个用户的详细情况时使用。")
public String getUserSummary(
@ToolParam(description = "用户ID") int userId) {
var users =
jdbc.queryForList(
"SELECT * FROM users WHERE id=?", userId);
if (
users.isEmpty())
{
return "用户ID " + userId + " 不存在";
}
var user =
users.get(0);
var stats =
jdbc.queryForMap("""
SELECT
COUNT(*) as total,
COALESCE(SUM(CASE WHEN status='completed'
THEN amount ELSE 0 END), 0) as spent,
COALESCE(SUM(CASE WHEN status='pending'
THEN 1 ELSE 0 END), 0) as pending
FROM orders WHERE user_id=?""", userId);
return
String.format("""
用户: %s (ID: %s)
邮箱: %s
套餐: %s
注册: %s
订单数: %s
已消费: ¥%.2f
待处理: %s""",
user.get("NAME"),
user.get("ID"),
user.get("EMAIL"),
user.get("PLAN"),
user.get("CREATED_AT"),
stats.get("TOTAL"),
stats.get("SPENT"),
stats.get("PENDING"));
}
}
4个工具各有分工:listTables看库里有啥表,describeTable看表结构,executeQuery执行任意SELECT,getUserSummary一键拉用户画像。
注意@ToolParam注解,它给每个参数加描述,AI靠这个描述理解参数含义。写清楚,AI传参就准;写模糊,AI就会乱传。
注意executeQuery里的安全检查。我做了两层拦截------第一层只允许SELECT开头,第二层扫描危险关键词。别小看这个设计,我第一次写的时候没加,AI帮我生成了一条DROP TABLE。还好连的是H2内存库,重启就恢复了。要是连的MySQL生产库,后果不敢想。从那以后再也不敢省安全检查。
打包部署,接入Claude
代码写好了,打包成JAR:
bash
mvn clean package -DskipTests
target/目录下会生成JAR文件。
方案一:Claude Desktop接入
找到Claude Desktop的配置文件。macOS路径:~/Library/Application Support/Claude/ [claude_desktop_config.json](http://claude_desktop_config.json) ,Windows路径:%APPDATA%\Claude\ [claude_desktop_config.json](http://claude_desktop_config.json) 。
打开,改成这样:
bash
{
"mcpServers": {
"database": {
"command": "java",
"args": [
"-jar",
"/你的路径/target/你的jar包.jar"
]
}
}
}
把路径换成你实际的,保存,重启Claude Desktop。打开Claude,直接说:"帮我查一下数据库里有什么表。"
Claude自动调用listTables,返回了"USERS, ORDERS"。它不是在猜,它真的启动了你的Spring Boot应用,真的连上了数据库,真的执行了查询。
再试一句:"Pro用户一共消费了多少?"Claude自动调用executeQuery,生成一条JOIN查询,执行后告诉你结果。你不需要写任何SQL。
我第一次跑通的时候,盯着屏幕愣了大概10秒。Java项目居然能被AI直接调用,这种感觉很奇妙------就像你写了一个REST API,但调用方不是前端,而是AI。
方案二:用Streamable HTTP远程部署
如果你的MCP Server要部署到服务器上,让多个客户端同时访问,换成HTTP传输。把 [pom.xml](http://pom.xml) 里的Starter换掉:
bash
<dependency>
org.springframework.ai
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
[application.properties](http://application.properties) 加一行:
bash
spring.ai.mcp.server.protocol=STREAMABLE
打包部署到任何支持Servlet的服务器,Tomcat、Jetty、Docker都行。Claude Desktop配置改成HTTP方式:
bash
{
"mcpServers": {
"database": {
"transport": "http",
"url": "http://你的服务器地址:8080/mcp"
}
}
}
代码不用改一行,只换一个Starter和一行配置,就从本地STDIO切到了远程HTTP。这就是Spring AI的威力。
我踩过的坑,你不用再踩
坑一:Spring AI版本不对。 spring-ai-bom版本要和Spring Boot版本匹配,Spring Boot 3.3配spring-ai-bom 1.0.0。版本不匹配会报各种奇怪的错误,最常见的是@Tool注解找不到。检查你的BOM版本,去Maven Central查最新的。
坑二:H2表名大小写问题。 H2默认把表名转大写,你写users,它存成USERS。查询时用information_schema,表名要传大写,否则查不到。我在describeTable里加了toUpperCase(),就是为了避免这个问题。
坑三:没加SQL安全检查。 跟Python版一样,什么SQL都放行,AI生成了DROP TABLE。还好是H2内存库,生产环境必须更严格,用只读数据库账号。
安全红线必须牢记:
-
只允许SELECT,禁止一切写操作
-
参数化查询,别直接拼接SQL
-
限制返回行数,别一次吐几万行
-
生产环境必须用只读账号
-
敏感字段(密码、token)不要返回 工具设计也有讲究。
@Tool的description要写具体,"处理数据库请求"太模糊,"列出数据库中所有表名,当用户想了解数据库结构时使用"才合格。AI靠这段话做路由决策,描述模糊它就会乱调。每个工具只做一件事,别搞一个manageDatabase包揽一切,拆成4个工具,AI才能精准选择。@ToolParam也别省,每个参数都写清楚含义,AI传参才不会出错。
总结:Java开发者的MCP入门路线
MCP让AI从只会说话,进化到能干活。Java开发者有天然优势------你现有的Spring Boot服务,加几个@Tool注解,打包成JAR,就能被任何AI客户端调用。Cursor、VS Code Copilot、Claude Desktop,全支持。
什么时候用MCP,什么时候用Function Calling?我的判断很简单:工具少于5个,只用一个模型,Function Calling够用。多人协作、多模型、工具多,上MCP。想接入Claude Desktop,必须用MCP。
Java版MCP的优势在哪?你的Spring Boot服务不用重写,不用新开一个Python项目,在现有Java代码里加注解就行。团队已有的Maven基础设施直接复用,公司安全合规流程不用重新走。
我建议你的下一步:基于本文Demo,把DatabaseToolsService里的JdbcTemplate换成你公司实际的数据库连接,连上你的MySQL或PostgreSQL,让AI直接查业务数据。或者写一个连CRM系统的MCP Server,让AI直接查客户信息。一旦用上,就回不去了。