SSE 握手 404?Claude Code 的 Base URL 按 TaoToken 通道改
SSE 握手阶段返回 404,是 MCP 从 STDIO 切到 SSE 之后最常见的一类接入故障。它的迷惑点在于:浏览器能打开页面,服务端日志也显示 Uvicorn 正常监听,可客户端一连/sse就直接 404。要快速定位问题,先把「模型通道」和「MCP 路由」拆开——用 TaoToken 给 Claude Code 配一条可用的模型通道(官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ),再让 Claude Code 对照你服务端里的create_starlette_app复现一次握手,就能判断 404 是出在路由侧还是 Base URL 写错。TaoToken 本身不参与/sse、/messages/的路由,它只保证你有可用的 Key 和正确的 Base URL,去把排障变量收敛到一个。
一、先定位:404 到底出在 /sse 还是模型通道
原文的服务端结构是 Starlette 起两个入口:一个Route("/sse", endpoint=handle_sse)负责持久连接,一个Mount("/messages/", app=sse.handle_post_message)负责接收工具调用请求。SSE 传输层由SseServerTransport("/messages/")构造,这个/messages/是客户端回传 POST 的基路径,而不是握手路径。很多人排障时把这两个路径搞混,于是改错了地方。
一次完整的 SSE 握手,客户端实际经历的是:
- 向
http://<host>:<port>/sse发 GET,并带上Accept: text/event-stream; - 服务端通过
sse.connect_sse(request.scope, request.receive, request._send)建立流,返回read_stream和write_stream; - 服务端先推一条
event: endpoint,data里是带session_id的/messages/地址; - 客户端拿着这个地址回去 POST 工具调用,再由 MCP Server 的
mcp_server.run(...)消费。
只要第 1 步返回 404,后面三步都不会发生。此时有两种可能:一种是 MCP 服务端确实没有注册/sse,另一种是客户端请求的 host、port、path 拼错了——这类错误在现场表现为「服务端明明写了/sse,客户端却连不上」。
怎么区分?用一条最笨但最有效的命令:
curl -i -N http://127.0.0.1:8081/sse如果返回404 Not Found,说明路由不匹配,问题在服务端或端口;如果返回200且持续输出event:行,说明/sse是通的,那么客户端报 404 就只能是自己拼出来的 URL 不对。注意这里必须用127.0.0.1或容器实际映射出来的地址,不要用浏览器地址栏里的域名去猜。
而 Claude Code 这一侧,它根本不关心你的 MCP 服务端怎么写。它要连的是模型通道。把两件事混在一条 Base URL 里排查,就会越查越乱。
二、TaoToken 前置:先拿一个可用的 Key 再做对照实验
在动 MCP 服务端之前,先让 Claude Code 的模型通道跑起来。打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,进入控制台创建 API Key,把 Key 复制出来备用(下文用YOUR_API_KEY代替)。
这里要强调一点:TaoToken 不接管/sse,也不接管/messages/。它只提供 Anthropic 兼容的模型调用入口,地址是:
https://taotoken.net/api所以你在 Claude Code 里要改的是ANTHROPIC_BASE_URL,而不是 MCP 服务端的路由。这样做的价值在于做「变量隔离」:当模型通道是确定可用的时候,你再让 Claude Code 去复现一次 SSE 握手,任何 404 都会被精确归因到 MCP 服务端或客户端 URL 拼接,而不是被「Key 不通」「Base URL 写错」这类噪音干扰。
对照原文时,保持create_starlette_app原样不动。它的路由是/sse加/messages/,这两个路径不会因为你换了模型通道而改变。真正需要复现的是「客户端怎么拼这个 URL」:
from mcp.client.sse import sse_client async with sse_client("http://127.0.0.1:8081/sse") as (read, write): ...如果sse_client里写的是http://127.0.0.1:8081/messages/,或者写成了http://127.0.0.1:8081/sse/,甚至端口还是示例里的8080,那就等着收 404。
拿到 Key 之后,建议先做一次纯模型请求的连通性验证,把模型通道钉死。Key 的管理入口在这里:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。
三、可复制配置:Claude Code 的 settings.json 与 ANTHROPIC_*
Claude Code 读取的配置文件通常在用户目录下的.claude/settings.json。把模型通道相关变量写进env字段,示意如下:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "替换为你在模型列表页看到的实际模型 ID", "ANTHROPIC_SMALL_FAST_MODEL": "替换为你在模型列表页看到的实际模型 ID" } }几个容易踩坑的点:
第一,ANTHROPIC_BASE_URL只写到/api,不要再手动补/v1。客户端会自己在后面拼/v1/messages,写重复了就会出现类似/api/v1/v1/messages的路径,表现为 404 或者 405。
第二,鉴权字段用ANTHROPIC_AUTH_TOKEN。如果你同时导出了ANTHROPIC_API_KEY,有两个来源的凭据会让行为变得不可预测,建议只保留一个,尤其是排障阶段。
第三,环境变量的优先级通常高于配置文件。在终端里敲env | grep ANTHROPIC看看有没有残留的旧值;Windows PowerShell 用Get-ChildItem Env:ANTHROPIC*。如果 shell 里还留着指向另一个地址的ANTHROPIC_BASE_URL,那么你改settings.json是无效的。
临时验证可以用一次性环境变量,不污染长期配置:
# macOS / Linux export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_AUTH_TOKEN="YOUR_API_KEY"# Windows PowerShell $env:ANTHROPIC_BASE_URL = "https://taotoken.net/api" $env:ANTHROPIC_AUTH_TOKEN = "YOUR_API_KEY"配置完先别急着回到 MCP 那边,先确认模型通道能通,这是后面所有排障的基准点。
四、验证请求与成功结果
第一步,验证模型通道。Anthropic 兼容端点是$ANTHROPIC_BASE_URL/v1/messages,用 curl 打一发最小请求:
curl -sS https://taotoken.net/api/v1/messages \ -H "x-api-key: YOUR_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "替换为实际模型 ID", "max_tokens": 32, "messages": [{"role": "user", "content": "ping"}] }'成功时你会拿到结构化 JSON,里面有content数组和usage字段。如果这里返回 401,那是 Key 的问题;返回 404,先检查是不是把地址写成了/api/v1/messages/messages或者落了/api。
第二步,验证 MCP 的 SSE 端点是否真的挂在/sse上:
curl -i -N http://127.0.0.1:8081/sse成功的表现是 HTTP 头里出现content-type: text/event-stream,并且连接保持不关闭,随后能读到形如event: endpoint的推送,data里带着/messages/?session_id=...。看到这一行,说明create_starlette_app里的Route("/sse", endpoint=handle_sse)已经生效,服务端没问题。
第三步,把客户端指向这个地址,把sse_client的入参写成完整 URL,用127.0.0.1而不是localhost。很多环境下localhost会优先解析到 IPv6 的::1,而你的 Uvicorn 只监听了 IPv4 的0.0.0.0,于是连接被拒或者被反向代理接管后返回 404。这一步能过,说明 SSE 握手链路已经打通。
第四步,观察日志。Starlette 在收到未知路径时会直接返回 404,而/sse命中时会进入handle_sse。对照两侧日志,就能明确 404 是「路由未命中」还是「模型通道配置错」。
五、本篇常见错排查
围绕 SSE 握手 404,下面这些是最高频的原因,按顺序排查能省掉大量时间。
路径拼错。
/sse是握手地址,/messages/是回传地址。把sse_client的入参写成了/messages/,请求会落到Mount上,而Mount只接受 POST,GET 自然 404。尾斜杠差异。
Route("/sse")匹配的是/sse,不是/sse/。Starlette 默认的redirect_slashes在这种流式请求里不一定能救回来,客户端如果不会跟随 307,就会直接看到失败。端口不对。示例里的默认端口是
8081,但很多容器或本地服务实际跑在别的端口。Docker 场景下尤其要注意EXPOSE 8081和-p 8081:8081是否都做了映射,只写EXPOSE不等于对外可达。监听地址不对。
uvicorn.run(app, host="0.0.0.0", port=8081)才能被外部访问;如果写成127.0.0.1,容器外或局域网内都连不上,客户端得到的往往是连接失败或被中间层改写成 404。Base URL 多写了
/v1。ANTHROPIC_BASE_URL应为https://taotoken.net/api,不要再加后缀。这一条和 SSE 的 404 长得很像,但排查的是两个完全不同的对象,务必先分清。鉴权字段混用。
ANTHROPIC_AUTH_TOKEN和ANTHROPIC_API_KEY同时存在时,行为取决于客户端实现,容易造成「明明 Key 是对的却报错」的假象。残留的代理变量。shell 里的
HTTP_PROXY、HTTPS_PROXY或某些 IDE 插件注入的环境变量会把请求引到别处,导致路径被重写。排障时先临时 unset 掉再看结果。配置文件位置写错。
settings.json放错目录等于没改,用claude启动时观察它读取的配置路径,确认改动真的生效。
按「先模型通道、再 SSE 端点、最后客户端 URL」的顺序走,404 的归属会非常清楚。模型通道不通,先解决鉴权与 Base URL;模型通了但/sse返回 404,那是服务端路由或监听配置;curl能拿到event: endpoint而客户端不行,那就是客户端拼 URL 的问题。
六、把 404 归位:固定你的排障顺序
回到这篇的标题:SSE 握手 404,改的往往不是服务端路由,而是 Claude Code 这一侧的模型通道配置。create_starlette_app里的Route("/sse")和Mount("/messages/")不需要因为换通道而改动,TaoToken 也不参与这两个路径的转发。它做的事情是把 Claude Code 的ANTHROPIC_BASE_URL指向https://taotoken.net/api,让你在排查 404 时有一个确定可用的基准,从而把「路由问题」和「地址写错」彻底分开。
如果你还在反复对着 404 猜原因,建议先按上面的顺序把两件事各自验证一遍:Key 从 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 创建和管理,配置细节对照接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 里的字段说明,尤其是 Base URL 写法与鉴权头。把settings.json里的ANTHROPIC_*改对,再让 Claude Code 复现一次/sse握手,404 到底属于哪一侧,一次就能看清。