ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

McpClientProvider 调 /mcp/sse 正常,ChatModel 却报 401?TaoToken 这样改 apiUrl 与 Key

McpClientProvider 调 /mcp/sse 正常,ChatModel 却报 401?TaoToken 这样改 apiUrl 与 Key jFinal 里用 SolonMCP 搭的 MCP 服务端/mcp/sse一调就通getWeather也能正常回“晴14度”可偏偏ChatModel.of(apiUrl)换到统一模型通道时就抛 401。排障第一步先记一件事去 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建 KeyTaoToken 的接口 Base URL 是https://taotoken.net/api绝对不能把官网页面地址填进ChatModel.of()。这篇不做泛泛的 MCP 入门只解决原文第 3、4 节里那个最容易被忽略的坑已经有一份能跑通的McpClientProvider也有一份能跑的ChatModel.of(apiUrl).provider(ollama).model(...)当你把模型提供者从本机 Ollama 换成线上统一通道时apiUrl到底该写什么、Key 该放到哪一层、为什么会出现 401/404、以及defaultToolsAdd(toolProvider)里的 MCP 工具集要不要跟着改。读完之后你应该能做到MCP 工具集继续由本机McpClientProvider提供模型请求单独走 TaoToken 通道两条链路各管各的互不干扰。1. getWeather 能返回ChatModel.of 却报 401先把两个 apiUrl 分开1.1 复现现场同一个变量名两条完全不同的链路原文第 3 节的客户端代码是这样的构造一个McpClientProviderapiUrl指向http://localhost:8080/mcp/sse然后callToolAsText(getWeather, map)拿到文本结果再顺手readResourceAsText(config://app-version)读个资源。这一段没毛病因为它走的是 MCP 协议里的 SSE 通道服务端是你在 2.x 节里刚起的 Undertow。到了第 4 节事情变得微妙。代码里出现了第二个apiUrl写法是private static final String apiUrl http://127.0.0.1:11434/api/chat;配provider ollama、model qwen2.5:1.5b然后ChatModel.of(apiUrl).provider(provider).model(model).defaultToolsAdd(toolProvider).build()。两个apiUrl名字一样含义完全不同前者是 MCP 服务端点后者是 LLM 的推理接口。很多人就是在“换个模型提供者”这一步把两者合并了。想换成线上通道时直接把ChatModel.of(...)里的常量替换成一个看起来像“平台地址”的字符串Key 又忘了传于是 401 就来了。把两条链路在代码里改成两个明确命名的常量是止血的第一步。1.2 401 和 404 分别指向哪里401 的意思很直白服务端认得这个地址但没收到合法的凭证或者收到的凭证不属于这个通道。落到本篇场景通常是三种情况Key 压根没传、Key 传到了McpClientProvider那一层而不是ChatModel那一层、或者apiUrl写成了一个不接受这种鉴权的页面地址。404 则通常跟鉴权无关是路径拼错了。最常见的两种把 Base URL 写成https://taotoken.net/api/v1导致实际请求路径变成/api/v1/chat/completions这类重复前缀或者把给人点的落地页地址填进了接口字段路径自然对不上。记住一个分工https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end是浏览器里打开用来注册、建 Key、看模型列表的https://taotoken.net/api才是塞进代码里的接口地址。2. SolonMCP 那一侧不用动McpServerEndpoint 还是老样子2.1 HelloApp 里必须开的异步开关先确认服务端这头是干净的。原文 2.1 节的入口类继承JFinalConfig用UndertowServer.create(HelloApp.class)起服务端口 8080关键在于onDeploy里给 jfinal 过滤器开了异步支持。MCP 内部是响应式实现同步过滤器会把 SSE 的长连接堵住不开这个开关/mcp/sse很快就会断。public class HelloApp extends JFinalConfig { public static void main(String[] args) { UndertowServer.create(HelloApp.class) .setDevMode(false) .setPort(8080) .onDeploy((cl, di) - { // MCP 是响应式模型jfinal 过滤器必须开异步 di.getFilters().get(jfinal).setAsyncSupported(true); }) .start(); } public void configPlugin(Plugins me) { me.add(mcpServerConfig); } public void configHandler(Handlers me) { me.add(mcpServerConfig); } private McpServerConfig mcpServerConfig new McpServerConfig(); }2.2 McpServerConfig只拦 /mcp/ 前缀的那段路由McpServerConfig同时实现IPlugin和Handlerstart()里用Solon.start(...)拉起 Solon 容器并加载mcpserver.ymlstop()里做Solon.stopBlock(false, ...)的优雅停机。handle()方法只处理target以/mcp/开头的请求其余原样交给next.handle(...)。这段逻辑跟 401 一点关系都没有所以排障时不要来这里乱改。public class McpServerConfig extends Handler implements IPlugin { public boolean start() { Solon.start(McpServerConfig.class, new String[]{--cfgmcpserver.yml}); return true; } public boolean stop() { if (Solon.app() ! null) { Solon.stopBlock(false, Solon.cfg().stopDelay()); } return true; } Override public void handle(String target, HttpServletRequest req, HttpServletResponse resp, boolean[] isHandled) { if (target.startsWith(/mcp/)) { Context ctx new SolonServletContext(req, resp); try { Solon.app().tryHandle(ctx); if (isHandled ! null isHandled.length 0) { isHandled[0] true; } } finally { ContextUtil.currentRemove(); } } else if (next ! null) { next.handle(target, req, resp, isHandled); } } }2.3 McpServergetWeather 就挂在这个端点下工具类是整条链路里最轻的一环一个注解加几个方法。McpServerEndpoint(sseEndpoint /mcp/sse)声明了客户端要连的路径ToolMapping声明工具Param(description ...)给参数加描述。想让模型正确挑到getWeather描述字段写清楚比方法名写得好更重要。编译参数建议打开-parameters否则参数名拿不到调用时可能对不上。McpServerEndpoint(sseEndpoint /mcp/sse) public class McpServer { ToolMapping(description 查询天气预报) public String getWeather(Param(description 城市位置) String location) { return 晴14度; } ResourceMapping(uri config://app-version, description 获取应用版本号) public String getAppVersion() { return v3.2.0; } PromptMapping(description 生成关于某个主题的提问) public CollectionChatMessage askQuestion(Param(description 主题) String topic) { return Arrays.asList(ChatMessage.ofUser(请解释一下 topic 的概念)); } }服务端四个文件确认完就可以把注意力全部转到客户端那句ChatModel.of(apiUrl)上。记住/mcp/sse通只能证明工具集可用完全不能证明模型通道的地址和 Key 是对的。3. ChatModel 的 apiUrl 换成 https://taotoken.net/apiKey 单独放3.1 先去把 Key 和模型 ID 拿到手打开 TaoToken 注册登录进控制台创建一把 API Key本文统一用YOUR_API_KEY占位别把真 Key 提交进仓库。同一页面能看模型广场MODEL_ID就从那里的当时列表里挑一个可用的对话模型不要凭记忆写gpt-5或者自己加日期后缀那类字符串在通道里通常直接 404。如果你只是先验证链路能不能通先记两个值就够了接口地址https://taotoken.net/api以及一把 Key。模型 ID 等你要跑代码时再回模型广场抄一次比提前写死在常量里靠谱。3.2 三种 apiUrl 写法对照写进ChatModel.of(...)的值结果https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end401 或 404这是给人点的页面https://taotoken.net/api/v1路径前缀重复容易 404https://taotoken.net/api正确末尾不带/v1表格第三行就是本篇唯一要你改的地方。原文那个http://127.0.0.1:11434/api/chat是 Ollama 自己的完整端点换到兼容通道时不要照着它的形状去拼/api/chat或/api/v1直接把 Base URL 交给 SDK它会自己补后面的路径。3.3 改写后的 McpClientTest下面这份是原文第 4 节代码的通道替换版。两处注意MCP_SSE_URL保持本机不变MODEL_BASE_URL换成统一通道Key 从环境变量读不写进源码。provider与apiKey的具体字段名以你当前使用的 solon-ai 版本为准如果版本里叫别的名字在对应 provider 配置项里找等效字段。public class McpClientTest { // MCP 工具集仍然连本机 SolonMCP不要换成通道地址 private static final String MCP_SSE_URL http://localhost:8080/mcp/sse; // 模型通道统一入口的 Base URL末尾不要加 /v1 private static final String MODEL_BASE_URL https://taotoken.net/api; // 模型 ID 以模型广场当时列表为准 private static final String MODEL_ID YOUR_MODEL_ID; public static void main(String[] args) throws Exception { McpClientProvider toolProvider McpClientProvider.builder() .apiUrl(MCP_SSE_URL) .build(); ChatModel chatModel ChatModel.of(MODEL_BASE_URL) .provider(openai) .apiKey(System.getenv(TAOTOKEN_API_KEY)) .model(MODEL_ID) .defaultToolsAdd(toolProvider) .build(); ChatResponse resp chatModel.prompt(杭州今天的天气怎么样).call(); System.out.println(resp.getMessage()); } }3.4 Key 不进代码环境变量加本地配置export TAOTOKEN_API_KEYYOUR_API_KEYKey 从 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建后复制落到本地环境变量或者 IDE 的运行配置里~/.zshrc、~/.bashrc都行别写进McpServerConfig也别挂到McpClientProvider上。MCP 客户端连的是你本机的 8080本来就不需要外部 Key把它传进去只会让人误以为“两个地址用同一套凭证”下次排障更乱。4. 401/404 对照表本篇三个高频错法4.1 401Key 根本没参与这次请求症状是服务端回鉴权失败日志里能看到请求确实到了通道但没带Authorization。原因通常是apiKey(...)这一行漏了或者写在了McpClientProvider的 builder 上。另一种情况是环境变量没生效——IDE 里改了运行配置但没重启进程System.getenv返回nullSDK 就不会带凭证。4.2 404路径前缀重复或多写一层把官网落地页地址填进ChatModel.of(...)或者写成https://taotoken.net/api/v1走的路径就会和 SDK 自己拼的路径叠起来。判断方法很简单让 SDK 打印一次实际请求的完整 URL和https://taotoken.net/api开头的拼接结果对一下多出来的那段就是问题所在。4.3 /mcp/sse 自己 404别往 Key 上想如果连McpClientProvider那步也开始 404方向就完全不同了。检查McpServerConfig.handle()里的target.startsWith(/mcp/)有没有写错McpServerEndpoint(sseEndpoint /mcp/sse)有没有被扫到以及 Undertow 端口是不是被别的进程占了。这三项跟模型通道、跟 Key 都没有关系不要因为两个地址都叫apiUrl就混在一起查。5. 排完错怎么验证先页面发一条再回 Java 跑 prompt5.1 用同一把 Key 在模型对话里验一遍代码改完先别急着跑main。打开 TaoToken 模型对话用刚才那把 Key 和同一个模型 ID 发一条测试消息。这一步能把问题范围瞬间缩小页面通、Java 不通说明是代码里的地址或 Key 传参有问题页面也不通说明 Key 或模型 ID 本身就不对。5.2 回到 Java 侧看工具调用有没有被带上main跑起来后重点看两件事getWeather有没有被模型选中并调用以及最终回答里有没有把“晴14度”这个工具结果串进去。如果模型回了话但没调工具检查工具描述和方法参数如果压根没回话回到第 4 节的 401/404 对照表。5.3 在控制台对账确认这次调用记上了调用成功之后建议回一次 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 的用量页面看这次请求有没有落到记录里。有记录说明链路是真的通了不是某种缓存假象没记录但代码有输出就要回去确认MODEL_BASE_URL是不是又被改回了带/v1的写法。排障做完整闭环下次换模型才不用重新猜。6. 工具集留本机模型请求走通道这套组合的长期姿势6.1 MCP 工具只能生成和解释不能替你去连生产库getWeather这类示例工具无所谓但如果你的 MCP 工具后面接了数据库查询记住边界模型通过工具拿到的应该是“生成出来的 SQL 文本”真正执行要在你本地的 SQL 客户端或运维终端里做再把报错或结果贴回对话。任何写成“让模型直接连上库执行诊断”的用法都不该出现在生产环境。TaoToken 在这条链路里只承担模型请求这一小段。6.2 想换模型只动一个字符串这是把MODEL_BASE_URL单独抽成常量的好处。以后从对话模型换到另一个更擅长的模型只需要把MODEL_ID改成模型广场上当前可用的那个 IDMCP_SSE_URL和toolProvider那几行完全不用碰。MCP 工具集是稳定的模型是易变的把易变的部分收敛到一个常量上改动成本最低。6.3 jFinal SolonMCP 这套接下来怎么做先把本机/mcp/sse跑通再把ChatModel.of(...)指向https://taotoken.net/api中间不混用任何页面地址。准备长期拿它写代码的话可以去 Coding Plan 看看套餐是否够用需要新建或轮换 Key 就在 控制台 API Keys 操作如果你还想把这套通道搬到终端里Claude Code 的变量写法可以照 接入文档 对一遍ANTHROPIC_BASE_URL同样是https://taotoken.net/api不要带/v1。
返回列表