1. 从一次“查 IP 归属地”的重复劳动说起
你可能遇到过这种场景:排查线上日志,看到一堆来源 IP,想快速知道它们分别属于哪个省市、哪家运营商。手动打开网页一个个查,查十几个就烦了。更麻烦的是,团队里每个人都在用不同的 AI 工具,有人用 Cherry Studio,有人用 Cline,有人用 Claude Code,如果能把“查 IP 归属地”做成一个标准工具,让这些客户端都能直接调用,那才叫省事。
这就是 MCP Server 的价值。MCP(Model Context Protocol)是让 AI 客户端发现并调用外部工具的一套协议,而 SSE 模式则让服务端以 HTTP 长连接的方式把工具能力暴露出去,客户端只要填一个 URL 就能接入。本文要做的,就是用 SpringAI + SpringBoot + JDK 17 搭一个 SSE 模式的 MCP Server,对外提供“根据 IP 查归属地”的工具,并且把服务端调用外部 API 的鉴权通道统一收敛到 TaoToken 的 Key 上,这样你只需要维护一份 Key,就能同时管住模型调用和工具调用。
适合谁看:有 Java 基础、想快速跑通一个 MCP Server 的后端同学;正在用 Cherry Studio / Cline / Claude Code 等客户端、想自己扩展工具能力的 AI 重度用户;以及想把内部小工具标准化暴露给 AI 的团队开发者。整条链路我会给到可复制的application.yml、工具注册代码、SSE 连接验证步骤和 curl 调用示例,目标是一次跑通。
先说清楚一个概念,避免后面混淆。MCP Server 本身不负责“理解自然语言”,它只负责“暴露工具”。真正把用户那句“122.224.33.10 属于哪里”翻译成工具调用的,是 MCP Client 背后的大模型。所以一个完整的链路是:用户在客户端提问 → 客户端把可用工具列表发给模型 → 模型决定调用获取ip归属地工具 → 客户端通过 SSE 把调用请求发到你的 Server → Server 执行查询并返回结果 → 模型组织成自然语言回复。理解这条链路,后面排查问题会轻松很多。
2. TaoToken 统一 Key 接入:把外部 API 通道收敛到一处
在动手写代码之前,先把“鉴权通道”这件事想明白。很多教程里,工具类直接硬编码某个第三方 API 的地址,或者把 Key 写在代码里,这在个人 demo 阶段没问题,但一旦工具有多个、要换供应商、要做额度统计,就会变成一团乱麻。我的做法是:所有对外请求统一走 TaoToken 的 API 通道,用同一个 Key 管理。
TaoToken 在这里扮演的角色是“统一入口”。你可以在它的控制台里创建 API Key,然后无论是模型对话、还是工具内部要调用的外部能力,都通过这个 Key 走同一个 Base URL。这样做的好处很直接:Key 只有一份,泄露了只需在一个地方轮换;额度、调用记录集中可见;换供应商时改配置而不是改代码。
具体到操作层面,你需要先拿到两样东西:Base URL 和 API Key。Base URL 是https://taotoken.net/api,注意这个地址不带任何查询参数,是纯粹的 API 根路径。API Key 则要去控制台创建,入口在https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite,进去之后点“创建 Key”,复制出来保存好,它只会完整显示一次。
这里有个容易踩的坑:很多人把官网首页地址和 API 地址搞混。官网是https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite,用来注册、看文档、进控制台;而代码里配置的 Base URL 必须是https://taotoken.net/api,两者不能互换。我第一次配的时候就填了首页地址,结果请求一直 404,排查了半天才发现是路径问题。
关于模型 ID,如果你后续想让 MCP Server 内部也调用模型做二次处理(比如把归属地结果翻译成更友好的描述),可以在配置里指定 Model ID。常见的做法是把它和 Base URL、Key 一起放进application.yml,三件套齐了才能正常发起请求。下面这段就是标准的配置形态,你可以直接抄:
taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} model-id: your-model-id注意api-key我用了环境变量占位,这是为了避免把 Key 提交到 Git。本地调试时可以在 IDEA 的 Run Configuration 里加环境变量,或者用.env文件配合启动参数。生产环境则应该走配置中心或密钥管理服务,不要明文写在 yml 里。
还有一点值得强调:TaoToken 的 Key 是“统一 Key”,意味着你不需要为每个工具单独申请一套凭证。IP 归属地查询这个工具内部如果要调外部接口,也走同一个通道。这样你的application.yml里只有一处鉴权配置,维护成本最低。如果你团队里有人用 Claude Code 做编码,有人用 Cline 做 Agent,他们各自的客户端配置里填的也是同一个 Base URL 和 Key,只是 Model ID 可能不同。这种“一处配置、多处复用”的模式,是我实测下来最省心的。
3. 可复制配置:application.yml 与 MCP 工具注册代码
这一节是全文的核心,我会把项目结构、依赖、配置、工具类、服务类、配置类全部给全,你照着贴就能跑。环境基线是 JDK 17、SpringBoot 3.5.0、SpringAI 1.0.0。
先看项目依赖。用 Spring Initializr 建项目时勾选 “Model Context Protocol Server”,生成的项目默认只支持 stdio 模式。要支持 SSE,必须额外引入 WebMVC 的 starter:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> </dependency>这个依赖很关键,少了它你的/sse端点根本不会注册,启动后访问会直接 404。我见过不少人卡在这一步,以为是配置写错了,其实是依赖没加。
接下来是application.yml,这是整篇文章最该复制的一段:
server: port: 7080 spring: ai: mcp: server: name: ip-location-mcp-server version: 1.0.0 sse-endpoint: /sse taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} model-id: your-model-id逐行解释一下。server.port设成 7080,你可以改成任何没被占用的端口。spring.ai.mcp.server.name和version是服务标识,客户端连接后能看到。sse-endpoint定义了 SSE 的访问路径,最终地址就是http://localhost:7080/sse。下面的taotoken段是我们自定义的,用来存放统一 Key 的三件套。
然后是工具类,负责真正去查 IP 归属地。这里我用一个 HTTP 工具发 GET 请求,把返回的 JSON 解析成 DTO:
@Slf4j public class IpSearchUtils { public static String getLocation(String ip) { String url = "https://opendata.baidu.com/api.php?query=" + ip + "&co=&resource_id=6006&oe=utf8"; try { String jsonStr = HttpUtil.get(url); IpInfoDto dto = JSONUtil.toBean(jsonStr, IpInfoDto.class); if (dto != null && dto.getData() != null && dto.getData().length > 0) { return dto.getData()[0].getLocation(); } return null; } catch (Exception e) { log.warn("获取ip位置信息失败", e); return null; } } }对应的IpInfoDto你可以根据实际返回的 JSON 结构生成,字段加好注释即可。这里不展开,因为不同数据源的字段名不一样,你按自己的响应体来。
接着是 MCP 服务类,用注解把方法声明成工具:
@Service public class IpSearchService { @Tool(name = "获取ip归属地", description = "根据ip获取归属地") public String getLocationByIp( @ToolParam(description = "ip地址, 使用ipv4的格式") String ip) { if (Objects.isNull(ip)) { return "错误:ip地址不能为空"; } String location = IpSearchUtils.getLocation(ip); if (Objects.isNull(location)) { return "错误:未查找到ip的归属地"; } return "ip归属地:" + location; } }@Tool的name和description会直接暴露给模型,所以描述要写清楚,模型才知道什么时候该调它。@ToolParam是参数说明,同样会传给模型,帮助它正确填参。
最后是配置类,把服务里的工具注册出去:
@Configuration public class McpConfig { @Bean public ToolCallbackProvider toolCallbackProvider(IpSearchService service) { return MethodToolCallbackProvider.builder() .toolObjects(service) .build(); } }没有这个 Bean,你的@Tool方法不会被 MCP 框架扫描到,客户端连接后工具列表是空的。这是第二个高频踩坑点。
如果你还想让工具内部调用模型,比如把归属地结果润色一下,可以在服务类里注入ChatClient,用taotoken.base-url和taotoken.api-key构造。但注意,MCP Server 的核心职责是暴露工具,模型调用是可选增强,不要本末倒置。
4. 验证请求:SSE 连接与 curl 调用示例
代码写完,启动应用。看到 SpringBoot 正常起来、端口 7080 监听成功,就可以验证了。
第一步,浏览器直接访问http://localhost:7080/sse。如果配置正确,你会看到连接保持打开,页面持续加载,控制台里能看到 SSE 的事件流。这说明 SSE 端点已经工作。如果返回 404,回去检查spring-ai-starter-mcp-server-webmvc依赖和sse-endpoint配置。
第二步,用 curl 验证工具调用。MCP over SSE 的调用分两步:先建立 SSE 连接拿到一个 session,再往消息端点 POST 请求。实际调试时,更简单的方式是直接用客户端。但为了让你理解底层,我给一个 curl 示例:
curl -N http://localhost:7080/sse-N表示禁用缓冲,这样你能实时看到服务端推送的事件。你会看到类似event: endpoint和data: /mcp/message?sessionId=xxx的输出,这个sessionId就是后续发消息要用的。
拿到 sessionId 后,发一个工具调用请求:
curl -X POST "http://localhost:7080/mcp/message?sessionId=你的sessionId" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "获取ip归属地", "arguments": { "ip": "122.224.33.10" } } }'如果一切正常,SSE 连接上会推回结果,内容里包含ip归属地:浙江省杭州市 电信这样的文本。这一步跑通,说明你的 MCP Server 已经能被程序调用了。
第三步,用 Cherry Studio 做端到端验证。打开 Cherry Studio,进设置 → MCP 服务器 → 添加服务器 → 快速创建。名称随便填,类型选sse,URL 填http://localhost:7080/sse。保存后点开工具选项卡,应该能看到获取ip归属地这个工具。然后新建对话,在工具栏里选中这个 MCP 服务器,输入“122.224.33.10 属于哪里?”,模型会自动调用工具并返回归属地。
如果你用的是 Cline 或 Claude Code,配置方式类似,都是填 SSE URL。区别在于 Claude Code 走的是settings.json配置,Cline 走的是 MCP 配置面板。无论哪个客户端,Base URL、Key、Model ID 这三件套的填写逻辑是一致的:Base URL 填https://taotoken.net/api,Key 填你创建的那串,Model ID 按需选。
验证成功的标志有三个:SSE 端点能连上、curl 能拿到工具返回、客户端对话能触发工具调用。三个都过,链路就通了。
5. 本篇常见错排查:401、local proxy failed 与工具列表为空
这一节我把实际踩过的坑列出来,对照报错找原因,能省你不少时间。
报错一:401 Unauthorized。这个最常见,原因通常是 Key 没配对或者没传。检查application.yml里的taotoken.api-key是否读到了环境变量,本地调试时环境变量没生效就会是空值。另一个可能是 Base URL 写成了官网首页而不是https://taotoken.net/api,路径不对会导致鉴权失败。还有一种情况是 Key 复制时带了空格,肉眼看不出来,建议重新复制一次。
报错二:local proxy failed 或连接被拒绝。这个报错通常出现在客户端侧,说明客户端连不上你的 SSE 端点。先确认服务真的起来了,netstat -ano | findstr 7080看端口有没有监听。如果端口正常,检查防火墙是否拦了本地回环。还有一种可能是你填的 URL 少了/sse后缀,或者用了https但服务是http。SSE 是长连接,中间如果有反向代理,要确保代理不缓冲、不超时。
报错三:reading choices 相关错误。这个一般出现在工具内部调用模型时,返回体结构不符合预期。检查你的 Model ID 是否填对,以及请求体格式是否符合 OpenAI 兼容规范。如果用的是 TaoToken 通道,Base URL 和 Key 正确的前提下,Model ID 写错会直接报模型不存在。
报错四:客户端工具列表为空。连接成功但看不到工具,八成是McpConfig里的ToolCallbackProviderBean 没生效。检查配置类有没有加@Configuration,IpSearchService有没有加@Service,以及@Tool注解有没有漏。还有一种情况是包扫描路径不对,配置类不在启动类的同级或子包下。
报错五:OAuth 相关提示。如果你在客户端配置时看到 OAuth 字样,说明客户端以为你要走 OAuth 鉴权。MCP SSE 模式下,鉴权是在你的 Server 内部处理的,客户端只需要填 URL。遇到这个提示,检查客户端配置里有没有误开 OAuth 选项,关掉即可。
报错六:Codex auth.json 配置问题。如果你同时用 Codex 类工具,它的auth.json里需要填 Base URL 和 Key。格式不对会导致鉴权失败。正确做法是把https://taotoken.net/api填到 base_url 字段,Key 填到 api_key 字段,Model ID 单独指定。三件套缺一不可。
排查的通用思路是:先看服务端日志,再看客户端日志,最后看网络层。服务端日志会告诉你请求有没有进来、工具有没有被调用;客户端日志会告诉你连接有没有建立、请求有没有发出。两边一对,问题基本就定位了。
6. 把工具接进你的日常 AI 工作流
跑通之后,你可以做几件事让它真正有用。第一,把 IP 归属地查询和其他工具合并到一个 MCP Server 里,比如加一个“查域名备案”或“查手机号归属地”,统一走 TaoToken 的 Key,这样客户端只需要配一个 SSE 地址就能用上全部工具。第二,把服务部署到内网一台常开的机器上,团队成员在各自客户端里填同一个 URL,省去每人本地起服务的麻烦。第三,如果你用 Coding Plan 做长期编码任务,可以把 MCP Server 的地址写进项目配置,让 Agent 在需要时自动调用。
关于 Key 的管理,我的建议是:开发环境用环境变量,生产环境用密钥管理,永远不要把 Key 提交到代码仓库。TaoToken 控制台可以创建多个 Key,你可以按项目或按人分配,方便轮换和审计。如果哪天 Key 泄露了,在控制台删掉重建即可,所有走这个 Key 的调用会立即失效,影响范围可控。
最后留一个实用技巧:SSE 连接是长连接,如果客户端长时间不用会断开,重连时 sessionId 会变。所以你的客户端配置里最好开启自动重连。另外,MCP Server 本身是无状态的,工具调用不依赖 session 里的上下文,所以水平扩展很容易,多起几个实例挂到负载均衡后面就行。
整条链路跑下来,你会发现最花时间的不是写代码,而是配环境和排查连接问题。代码部分其实很少,SpringAI 把 MCP 的协议细节都封装好了,你只需要关注工具逻辑本身。把鉴权通道收敛到 TaoToken 之后,后续加工具、换模型、做统计都变得简单。这套模式我已经在几个内部项目里用过,稳定性没问题,你可以放心照着搭。