欢迎光临
我们一直在努力

MCP协议实战(Java版):用Spring Boot让AI直接查你的数据库

摘要: 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) ,确认依赖存在:

<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) 里加两行:

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/下:

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自动执行:

spring.sql.init.mode=always

复制粘贴就行,别跳过。后面所有实操都基于这些数据——两张表,3个用户,5条订单。


写你的第一个MCP Server

先写最简版,一个方法,让你感受MCP是怎么回事。新建 [DatabaseToolsService.java](http://DatabaseToolsService.java) :

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就会乱调。

启动项目:

mvn spring-boot:run

没报错就成功了。想验证工具是否注册成功?用MCP Inspector,另开一个终端:

npx @modelcontextprotocol/inspector \\

–command "java" \\

–args "-jar target/你的jar包名.jar"

或者直接指向启动类:

npx @modelcontextprotocol/inspector \\

–command "mvn" \\

–args "spring-boot:run" \\

–cwd "你的项目路径"

浏览器打开http://localhost:5173,你会看到可视化界面,里面列出了listTables工具,还能直接点击测试。

看到这里你可能想:就这?别急,接下来注册4个工具,这才是重头戏。


完整版:4个工具全部注册

在 [DatabaseToolsService.java](http://DatabaseToolsService.java) 里补全所有方法:

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:

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) 。

打开,改成这样:

{

"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换掉:

<dependency>

org.springframework.ai

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

</dependency>

[application.properties](http://application.properties) 加一行:

spring.ai.mcp.server.protocol=STREAMABLE

打包部署到任何支持Servlet的服务器,Tomcat、Jetty、Docker都行。Claude Desktop配置改成HTTP方式:

{

"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直接查客户信息。一旦用上,就回不去了。

赞(0)
未经允许不得转载:171主机测评 » MCP协议实战(Java版):用Spring Boot让AI直接查你的数据库
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址