【SpringAI】8.通过json动态添加mcp服务:把MCP配置改到TaoToken

发布时间:2026/10/12 19:43:18
【SpringAI】8.通过json动态添加mcp服务:把MCP配置改到TaoToken 1. 为什么要把 MCP 配置从 yml 搬到 JSON 动态加载Spring AI 官方示例里MCP 服务基本都写在application.yml里靠spring.ai.mcp.client.stdio或sse这类前缀自动装配。这个方式在单机 demo 阶段很舒服但一旦进入真实项目就会卡住每加一个 MCP 服务就要改配置文件、重启应用多环境开发/测试/生产切换端点时yml 里塞满不同 profile 的覆盖项维护成本直线上升更麻烦的是运营侧想临时挂一个工具链还得让后端改代码重新发版。我试过在一个知识库项目里同时挂了 6 个 MCP 服务yml 写到 200 多行改一个 SSE 端点要翻半天。后来参考 Cherry Studio 的做法把 MCP 配置抽成 JSON 存到数据库前端提交 JSON、后端解析后动态注册到McpSyncClient热加载生效不用重启。这就是本文要讲的「通过 JSON 动态添加 MCP 服务」的完整实现路径。核心思路分三层第一层是数据层用一张ai_mcp表存config_json字段JSON 结构直接沿用开源 MCP 服务给出的mcpServers模板拿来即用第二层是解析层用 Jackson 把 JSON 拆成ServerConfig区分sse和stdio两种传输类型第三层是运行时层DynamicMcpClientManager维护一个ConcurrentHashMapString, McpSyncClient定时健康检查 断线重连工具回调带 30 秒缓存。这套方案适合谁适合已经在用 Spring AI 做 Agent、需要多环境快速切换 MCP 端点的开发者适合把 MCP 服务当运营配置项、希望前端可视化增删的产品也适合想把 MCP 工具链统一走一个 API 通道、避免每个服务单独配 Key 的团队。下面从依赖、建表、解析、动态管理到验证一步步给可复制的代码。2. TaoToken 前置统一 Key 与 API 通道的接入参数在讲动态注册之前先把 MCP 服务端点的来源说清楚。很多 MCP 服务尤其是模型对话类、coding 类需要填 Base URL 和 API Key如果每个服务单独申请、单独配Key 管理会非常散。TaoToken 提供的是统一 Key 和统一 API 通道官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。接入时你需要准备三件套这三件套在后面的 JSON 配置里会直接体现配置项值说明Base URLhttps://taotoken.net/api所有请求走这个入口不要带 UTM 参数API Key在控制台创建形如sk-xxxx用于鉴权Model ID按需选择例如对话类、coding 类模型 ID创建 Key 的入口在控制台的 API Keys 页面路径是 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 。如果你只是想先验证模型能不能通可以直接用模型对话页面 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 发一条消息试试。长期做编码或 Agent 的建议看 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 。这里要强调一点TaoToken 是统一的 API 通道不是让你绕过什么而是把多个 MCP 服务需要的模型调用收敛到一个 Key 上。你在 JSON 里配置 MCP 服务时如果这个服务本身要调模型就把 Base URL 填https://taotoken.net/apiKey 填你创建的 KeyModel ID 填对应模型。这样前端提交的 JSON 里就只出现一个 Key运维侧换 Key 只改一处。注意API 地址不要加 UTM 参数只有官网和 deep link 才带utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite。写进代码或 JSON 的 Base URL 必须是干净的https://taotoken.net/api。3. 可复制配置pom 依赖、建表 SQL 与 JSON 模板这一节给三份可直接复制的配置Maven 依赖、ai_mcp建表 SQL、以及前端提交的 JSON 模板。三份都对齐后面解析才不会出错。先看 pom 依赖。核心是spring-ai-starter-mcp-client它包含ToolCallbackProvider和McpSyncClient再补一个spring-ai-model拿ToolCallback接口。版本用你项目里统一的spring-ai.version。!-- Spring AI MCP 核心包 (包含 ToolCallbackProvider) -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId /dependency !-- Spring AI Model (ToolCallback 接口等) -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-model/artifactId version${spring-ai.version}/version /dependency建表 SQL 用 MySQLconfig_json字段类型是json直接存前端提交的原始 JSON。content_type存sse或stdiostatus用 1/0 表示启用禁用。CREATE TABLE ai_mcp ( id bigint NOT NULL AUTO_INCREMENT, content_type varchar(64) NOT NULL COMMENT 连接类型sse,stdio, name varchar(128) NOT NULL COMMENT mcp名称, description varchar(255) DEFAULT NULL COMMENT 功能描述, config_json json NOT NULL COMMENT 配置参数json类型, status tinyint(1) DEFAULT NULL COMMENT 启用状态1启用0不启用, update_time timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;JSON 模板是重点。开源 MCP 服务基本都给mcpServers结构你直接拿来用。下面这个模板里url是服务端点type是传输类型sseEndpoint是 SSE 路径。如果这个 MCP 服务需要调模型把headers里的 Authorization 填成 TaoToken 的 KeyBase URL 用https://taotoken.net/api。{ mcpServers: { amap-amap-sse: { url: https://mcp.amap.com, type: sse, sseEndpoint: /sse?keyxxxxxxxxxxxxxxxxxx }, taotoken-model-sse: { url: https://taotoken.net/api, type: sse, sseEndpoint: /sse, headers: { Authorization: Bearer sk-你的TaoTokenKey }, timeout: 30 } } }这份 JSON 提交到后端后AiMcpService.addMcp会解析mcpServers取第一个 server 名作为name取type作为content_type然后如果status1就调用dynamicMcpClientManager.addOrUpdateServerConfig动态注册。整个过程不需要重启前端刷新工具列表就能看到新工具。提示sseEndpoint如果服务方给的是完整 URL就填完整如果只给路径就填/sse这种。ServerConfig.getFullSseUrl()会自动拼接 base URL 和 endpoint避免双斜杠。4. 动态加载与验证从 JSON 到工具链调用的完整步骤配置就绪后验证分四步解析 JSON、动态注册、查询工具、调用工具。每一步都有对应的接口和日志出问题能定位。第一步解析 JSON。AiMcpService.addMcp里用ObjectMapper.readTree读configJson取mcpServers节点遍历每个 server调ConversionUtil.parseServerConfigFromJson转成ServerConfig。这里要处理type大小写sse和stdio都做equalsIgnoreCase。解析失败要打 warn 日志并跳过不能让一条坏配置拖垮整个加载。ObjectMapper objectMapper new ObjectMapper(); JsonNode root objectMapper.readTree(mcp.getConfigJson()); JsonNode serversNode root.path(mcpServers); if (serversNode.isObject()) { serversNode.fields().forEachRemaining(entry - { String serverName entry.getKey(); JsonNode serverNode entry.getValue(); String type serverNode.path(type).asText(); if (sse.equalsIgnoreCase(type) || stdio.equalsIgnoreCase(type)) { ServerConfig config ConversionUtil.parseServerConfigFromJson(serverName, serverNode, type); if (config ! null) { dynamicMcpClientManager.addOrUpdateServerConfig(serverName, config); } } }); }第二步动态注册。DynamicMcpClientManager.updateServerConfigs会对比新旧配置新增或变更的 server 走connectServer。SSE 传输用HttpClientSseClientTransport.builder(config.getUrl()).sseEndpoint(config.getSseEndpoint()).build()然后McpClient.sync(transport).requestTimeout(config.getTimeout()).build()最后client.initialize()。连接成功放进activeClients失败按MAX_RECONNECT_ATTEMPTS3重试重试间隔 2 分钟三次都失败就把数据库里status置 0避免死循环。HttpClientSseClientTransport transport HttpClientSseClientTransport.builder(config.getUrl()) .clientBuilder(HttpClient.newBuilder()) .sseEndpoint(config.getSseEndpoint()).build(); client McpClient.sync(transport) .requestTimeout(config.getTimeout()) .build(); client.initialize(); activeClients.put(serverName, client);第三步查询工具。McpToolController暴露了/mcp/tools接口返回当前所有可用工具。工具回调带 30 秒缓存getAvailableToolCallbacks里判断now - lastToolRefresh TOOL_CACHE_TTL才刷新。刷新时调SimpleMcpToolCallbackProvider.createToolCallbacks(clients)对每个 client 调listTools()把McpSchema.Tool包成SyncMcpToolCallback。curl -X GET http://localhost:8080/mcp/tools返回结构里toolCount是工具数tools数组每项有name、description、inputSchema。如果toolCount是 0先看/mcp/status里hasDynamicManager是不是 true再看日志里有没有连接MCP服务器失败。第四步调用工具。用/mcp/call/{toolName}POST body 是 JSON 参数。比如高德地图的搜索工具传{keywords:北京南站}返回结果里successtrue和result字段就是工具输出。curl -X POST http://localhost:8080/mcp/call/amap-amap-sse \ -H Content-Type: application/json \ -d {keywords:北京南站}最后一步是 ChatClient 接入。在getFluxVOFlux里如果body.getUseTools()为 true就取dynamicMcpClientManager.getAvailableToolCallbacks()塞进chatModel.prompt(prompt).toolCallbacks(toolCallbacks).stream().chatResponse()。这样模型在流式输出时就能自动调工具工具结果回填后继续生成。if (body.getUseTools()) { ListToolCallback toolCallbacks dynamicMcpClientManager.getAvailableToolCallbacks(); publisher chatModel.prompt(prompt).toolCallbacks(toolCallbacks).stream().chatResponse(); } else { publisher chatModel.prompt(prompt).stream().chatResponse(); }验证成功的标志/mcp/tools返回的toolCount大于 0/mcp/call/{toolName}返回successtrueChatClient 流式输出里能看到工具调用痕迹。如果工具列表为空先确认status1再确认 JSON 里type是sse或stdio。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth动态注册 MCP 时报错集中在四类鉴权 401、本地代理失败、响应解析 reading choices 报错、OAuth 授权失败。下面逐个对照真实日志给排查路径。第一类401 Unauthorized。日志里出现401或Unauthorized通常是headers里的 Authorization 没带或 Key 失效。检查 JSON 里headers.Authorization是不是Bearer sk-xxx格式Key 是不是在 TaoToken 控制台创建的。如果 MCP 服务本身不需要鉴权就不要加headers否则多余的头可能被服务端拒绝。还有一种情况是 Base URL 写成了带 UTM 的官网地址正确写法是https://taotoken.net/api不带任何参数。第二类local proxy failed。日志里出现local proxy failed或connection refused说明url或sseEndpoint拼出来的地址连不上。先用curl手动测一下完整 SSE URL比如curl -N https://mcp.amap.com/sse?keyxxx看能不能收到event: endpoint。如果 curl 通但 Java 不通检查HttpClientSseClientTransport的clientBuilder有没有设超时默认超时可能太短。另外sseEndpoint如果服务方给的是/sse?keyxxx要原样填不要只填/sse。第三类reading choices 报错。日志里出现Error reading choices或JsonParseException通常是 MCP 服务返回的不是标准 SSE 格式或者listTools()返回体被截断。先确认type填对了SSE 服务填ssestdio 服务填stdio。如果服务方返回的是 JSON-RPC 但 content-type 不是text/event-streamHttpClientSseClientTransport会解析失败。这时候要么换服务端点要么在ServerConfig里加自定义 header 强制Accept: text/event-stream。第四类OAuth 授权失败。日志里出现OAuth或invalid_token说明 MCP 服务走的是 OAuth 流程不是简单 Bearer。这种服务需要先拿 access token再放进headers.Authorization。如果服务方支持 API Key 直连优先用 API Key省掉 OAuth 刷新逻辑。TaoToken 的 Key 是长期有效的直接填Bearer sk-xxx即可不需要走 OAuth。排查时有个通用技巧把DynamicMcpClientManager的日志级别调到 DEBUGconnectServer里会打连接MCP服务器失败: {} - {} (url: {}, sseEndpoint: {})四个占位符分别是 server 名、异常消息、url、sseEndpoint。对着这四个值逐个核对基本能定位到是配置错还是网络错。注意如果三次重连都失败disableMcpInDatabase会把数据库status置 0。排查完配置后记得把status改回 1或者重新提交一次 JSON否则服务不会重新加载。6. 语义一致 CTA把 Key、文档和模型验证串起来动态 MCP 跑通后下一步是把 Key 管理和模型验证固定成流程。建议按这个顺序操作先在 API Keys 页面创建 Key https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 把 Key 填进 JSON 的headers.Authorization然后对着接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 核对 Base URL 和 Model ID 的写法接着用模型对话页面 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 发一条消息确认 Key 和模型 ID 能通最后回到/mcp/tools刷新工具列表确认工具链加载正常。如果你做的是长期编码或 Agent 场景Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_campaignrewrite 里对模型 ID 和调用方式有更细的说明配合动态 MCP 用起来更顺。整个链路里TaoToken 只承担统一 Key 和 API 通道的角色MCP 服务的注册、健康检查、工具回调还是由你的 Spring AI 应用管理职责边界清晰出问题也好定位。最后留一个实用技巧把ai_mcp表的config_json做成前端可编辑的文本框提交前用ObjectMapper校验一次 JSON 合法性不合法直接返回错误不要入库。这样能挡掉大部分因为手抖导致的解析失败比事后看日志排查省事得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询