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

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

相关推荐
雪隐1 小时前
个人电脑玩AI-06让5060 Ti给你打工——不光能画画,Qwen3-TTS还能学人说话,连我老板都信了!
人工智能·后端·python
Coffeeee2 小时前
帮你快速理解AI Agent之我想招个Android实习生
android·人工智能·agent
新新技术迷2 小时前
AI聊天自动跟随滚动,附回到底部按钮
人工智能
先锋部队2 小时前
用Web Worker解析AI返回的大文本不卡UI
人工智能
把你拉进白名单2 小时前
8.OpenClaw源码解析——三层洋葱重试
人工智能·llm·agent
用户632415031782 小时前
拖文档进AI对话框解析,前端要处理哪些脏活
人工智能
姗姗来迟了2 小时前
AI回答里的引用来源卡片,前端怎么做
人工智能
用户7106207733402 小时前
Codex-端口配置错误排查案例(stream disconnected before completion)
人工智能
IT_陈寒3 小时前
JavaScript的默认参数挖坑实录,我掉进去了
前端·人工智能·后端