1. 为什么要把数据库问答塞进 MCP server
先说清楚这篇在解决什么问题。你手上大概率已经有一个 MySQL 或者 PostgreSQL 的业务库,产品经理天天问「能不能让 AI 直接查数据」,你也不想每次都手写 SQL 再贴给大模型。MCP(Model Context Protocol)就是干这个的:它把「查数据库」这件事包装成一个标准工具,任何支持 MCP 的客户端都能调用,模型自己决定什么时候调、传什么参数。
但真正落地时会撞上两个坑。第一个坑是模型通道问题:MCP server 本身只负责暴露工具,真正做推理的大模型还得单独接一个 API,如果每个项目都去申请一套 Key、维护一套 Base URL,时间全耗在配置上。第二个坑是 Java 生态的 MCP 资料偏少,网上大量示例是 Python 或 Node 的,Spring AI 的 starter 又更新得快,版本对不上就报错。
所以这篇的路线是:用 Java 17 + Spring AI 搭一个 MCP server,把「查用户信息」和「执行 SQL」两个工具暴露出去,再让模型通过 TaoToken 的统一通道来调用。TaoToken 在这里的角色是统一 Key/API 通道——你不需要为每个模型单独配一套凭证,Base URL 和 Key 统一管理,MCP server 只管把工具做扎实。
适合谁看:有 Java 基础、写过 Spring Boot、想给内部系统加一个「自然语言查库」能力的后端同学。不需要你懂 MCP 协议细节,跟着配置走就行。下面从依赖开始,一步步到能跑通一次真实问答。
2. TaoToken 统一通道的前置准备
在写 MCP server 之前,先把模型通道这块理清楚,不然后面调试时分不清是工具没注册上还是 Key 不对。
TaoToken 的核心价值是把多家模型的调用收敛到一个入口。你注册后在控制台拿到一个 API Key,所有请求走同一个 Base URL,模型 ID 在请求体里指定。对 MCP 场景来说这特别省事:MCP server 暴露工具,客户端(比如 Cherry Studio 或者你自己写的 Agent)负责把工具描述和用户问题一起发给模型,模型返回要调用的工具名和参数,客户端再回调 MCP server。整条链路里,模型这一环只需要一个 Key。
具体操作路径是这样的。先打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 注册账号,进控制台。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,在里面找到 API Keys 页面,新建一个 Key 并复制保存。这个 Key 只在创建时完整显示一次,丢了就得重建。
拿到 Key 之后,你需要记住两个东西:Base URL 是 https://taotoken.net/api ,以及你要用的模型 ID。模型 ID 可以在模型对话页面先试一下,地址是 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,随便发一句话确认通道通不通。如果这一步就报 401,那说明 Key 复制错了或者没带上,先解决这个再往下走。
注意:MCP server 本身不直接调用模型,它是被客户端调用的。所以 TaoToken 的 Key 是配在客户端那一侧(比如 Cherry Studio 的模型设置里),不是配在 MCP server 的 application.yml 里。这一点很多人第一次会搞混,把 Key 写进 MCP server 配置然后发现根本用不上。
如果你打算长期跑编码类或 Agent 类任务,可以顺手看一下 Coding Plan 页面 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ,它针对高频调用场景做了额度设计,比按次计费更适合天天用的场景。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有各语言的调用示例,后面验证请求时会用到。
前置准备就这些:一个 Key、一个 Base URL、一个确认可用的模型 ID。接下来进入 Java 工程。
3. Spring AI MCP server 的可复制配置
这一节是全文最核心的部分,所有配置都可以直接复制。我用的版本组合是 Spring Boot 3.4.5 + Spring AI 1.1.0-SNAPSHOT + Java 17,传输方式选 WebFlux SSE,因为后面要在浏览器和客户端之间保持长连接。
先看 pom.xml。关键点是不要引入 spring-boot-starter-web,否则会和 WebFlux 冲突。依赖只需要 MCP server 的 webflux starter 和 MySQL 驱动。
<?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.4.5</version> <relativePath/> </parent> <groupId>cn.codemaven</groupId> <artifactId>mcp-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>17</java.version> <spring-ai.version>1.1.0-SNAPSHOT</spring-ai.version> </properties> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webflux</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.30</version> </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> <repositories> <repository> <id>spring-milestones</id> <url>https://repo.spring.io/milestone</url> </repository> <repository> <id>spring-snapshots</id> <url>https://repo.spring.io/snapshot</url> <snapshots><enabled>true</enabled></snapshots> </repository> </repositories> </project>然后是 application.yml。这里定义了 MCP server 的名称、SSE 端点、能力开关,以及数据库连接。SSE 端点 /sse 是客户端连接用的,/mcp/message 是消息回传用的,两个都要对上。
server: port: 8080 spring: ai: mcp: server: name: db-mcp-server version: 1.0.0 type: SYNC sse-endpoint: /sse sse-message-endpoint: /mcp/message instructions: "该服务器提供用户信息查询和数据库元数据工具" capabilities: tool: true resource: true prompt: true completion: true datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mcp_demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC username: root password: root数据库这边准备一张 user 表就够了,字段包括 user_id、username、email、mobile、status、dept_id、create_time。建表语句和几条测试数据:
CREATE TABLE `user` ( `user_id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `mobile` varchar(100) DEFAULT NULL COMMENT '手机号', `status` tinyint DEFAULT NULL COMMENT '状态 0:禁用 1:正常', `dept_id` bigint DEFAULT NULL COMMENT '部门ID', `create_time` datetime DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户'; INSERT INTO `user` (`user_id`, `username`, `email`, `mobile`, `status`, `dept_id`, `create_time`) VALUES (414, '钱睿', 'rqi2011@gmail.com', 'ePri67B4DK', 1, 504, '2002-12-24 22:57:04');工具类用 @Tool 注解暴露方法。UserService 负责按 ID 查用户,DatabaseMetadataService 负责返回库表结构和执行 SQL。两个类都通过 ToolConfig 注册成 ToolCallbackProvider。
@Service public class UserService { @Value("${spring.datasource.url}") private String dbUrl; @Value("${spring.datasource.username}") private String dbUsername; @Value("${spring.datasource.password}") private String dbPassword; @Tool(name = "queryUserInfo", description = "根据id查询用户信息") public Map<String, String> queryUserInfo(@ToolParam(description = "用户id") String id) { Map<String, String> resultMap = new HashMap<>(); String sql = "SELECT * FROM user WHERE user_id = ?"; try (Connection conn = DriverManager.getConnection(dbUrl, dbUsername, dbPassword); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, id); try (ResultSet rs = ps.executeQuery()) { ResultSetMetaData meta = rs.getMetaData(); int count = meta.getColumnCount(); while (rs.next()) { for (int i = 1; i <= count; i++) { resultMap.put(meta.getColumnLabel(i), String.valueOf(rs.getObject(i))); } } } } catch (SQLException e) { throw new RuntimeException("查询失败", e); } return resultMap; } }ToolConfig 把两个 service 都注册进去:
@Configuration public class ToolConfig { @Bean public ToolCallbackProvider userTools(UserService userService) { return MethodToolCallbackProvider.builder().toolObjects(userService).build(); } @Bean public ToolCallbackProvider databaseMetadataTools(DatabaseMetadataService metadataService) { return MethodToolCallbackProvider.builder().toolObjects(metadataService).build(); } }到这里配置就齐了。启动类就是普通的 @SpringBootApplication,跑起来后控制台会打印 MCP server 注册的工具列表,看到 queryUserInfo 和 executeSql 就说明注册成功。
4. 验证请求与成功结果核对
配置写完必须验证,不然你不知道是工具没注册还是模型没调对。分两步走:先验证 MCP server 本身,再验证整条问答链路。
第一步,启动应用后看日志。正常情况会输出类似这样的内容:
Registered tools: [queryUserInfo, queryDatabaseMetadata, executeSql] MCP Server started on port 8080, SSE endpoint: /sse如果只看到 server started 但没有工具列表,说明 ToolConfig 没被扫描到,检查包路径是否在启动类的同级或子包下。
第二步,用 curl 直接测 SSE 端点是否活着:
curl -N http://localhost:8080/sse正常会返回一串 event stream 数据,包含 endpoint 信息。如果连接被拒绝,检查端口占用和 WebFlux 依赖是否引入正确。
第三步,在客户端里配模型通道。以 Cherry Studio 为例,模型设置里填 TaoToken 的 Base URL 和 Key,模型 ID 选一个支持工具调用的(比如 claude 系列或 gpt 系列)。然后在 MCP 设置里添加服务器,类型选 SSE,URL 填 http://localhost:8080/sse,保存后切到工具标签,应该能看到三个工具。
第四步,发一次真实提问:「查询用户id为414的用户信息」。预期行为是:模型解析出要调用 queryUserInfo,参数 id=414,客户端向 MCP server 发请求,server 查库返回结果,模型根据结果组织成自然语言回答。
成功时你会看到类似这样的返回:
用户ID 414 的信息如下: - 用户名:钱睿 - 邮箱:rqi2011@gmail.com - 手机号:ePri67B4DK - 状态:1(正常) - 部门ID:504 - 创建时间:2002-12-24 22:57:04同时 MCP server 控制台会打印一次工具调用日志,包含方法名和参数。如果模型回答里没有真实数据,而是说「我无法查询」,那大概率是工具没被模型识别到,检查工具描述是否清晰、客户端是否真的把工具列表传给了模型。
再测一个复杂点的:「帮我查一下 user 表里 status 为 1 的用户有几个」。这个会走 executeSql 工具,模型生成 SELECT COUNT(*) FROM user WHERE status = 1,server 执行后返回数字。这一步能跑通,说明元数据工具和 SQL 执行工具都正常。
5. 常见报错排查
实际搭的时候会撞上几类典型错误,这里按报错原文对照排查。
401 Unauthorized:出现在客户端调模型时。原因通常是 Key 没填、填错,或者 Base URL 写成了带路径的形式。正确写法是 Base URL 只到 https://taotoken.net/api ,不要在后面加 /v1 或 /chat/completions,那些由 SDK 自己拼。检查 Key 是否有前后空格。
local proxy failed / connection refused:出现在客户端连 MCP server 时。说明 http://localhost:8080/sse 连不上。先确认应用真的启动了,再确认端口没被占用。如果是 Docker 里跑客户端、宿主机跑 server,localhost 要换成宿主机 IP。
reading choices 相关报错:模型返回体解析失败。常见于模型 ID 填错,或者用了不支持工具调用的模型。换一个明确支持 function calling 的模型 ID 再试。如果用的是流式模式,某些客户端对 SSE 解析有兼容问题,可以临时关掉流式验证。
OAuth 相关报错:如果客户端提示需要 OAuth 授权,说明它把 MCP server 当成了需要鉴权的远程服务。本地 SSE 不需要 OAuth,检查客户端配置里是否误开了认证选项。
工具列表为空:MCP server 起来了但客户端看不到工具。三个检查点:ToolConfig 是否被 Spring 扫描到、@Tool 注解的 name 是否重复、application.yml 里 capabilities.tool 是否为 true。
SQL 执行报语法错误:模型生成的 SQL 带了 markdown 代码块标记(比如sql)。在 executeSql 方法里先做一次清洗,去掉首尾的和语言标识再执行。
数据库连接超时:检查 MySQL 是否允许远程连接、时区参数是否带上 serverTimezone=UTC。如果报 Public Key Retrieval is not allowed,在 URL 后面加 allowPublicKeyRetrieval=true。
排查顺序建议从下往上:先确认数据库能连,再确认 MCP server 工具注册成功,再确认客户端能连上 SSE,最后确认模型通道正常。每一层都有独立的验证方法,不要跳步。
6. 把通道固定下来,后面就省事了
整套跑通之后,你会发现真正花时间的不是写工具类,而是把模型通道、MCP 端点、数据库连接这三者的配置对齐。一旦对齐,后面加新工具就是复制一个 @Tool 方法的事。
我的建议是把 TaoToken 的 Key 和 Base URL 统一放在客户端的模型配置里,MCP server 只关心数据库和工具逻辑,两边职责分开。这样换模型、换客户端都不用动 server 代码。API Keys 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ,需要新建或轮换 Key 时从这里进。接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 里有不同语言的请求示例,调不通的时候对照一下请求体格式。
如果你后面要接 Claude Code 这类编码工具,Anthropic 兼容端点也在同一套通道下,配置方式在 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude_code&utm_campaign=rewrite 有说明。核心还是那三件套:Base URL、Key、Model ID,填对就能用。
最后留一个实用技巧:给 executeSql 加一个只读白名单,只允许 SELECT 开头,其他语句直接拒绝。生产库上跑自然语言查询,安全边界比功能本身更重要。