fix(mcp): 修正 Claude Code / Cursor 客户端配置格式(联网核实 2026)
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled

依据 2026 官方文档联网核实所有 MCP 客户端配置格式,发现两处错误:

1. Claude Code .mcp.json:type 值应为 "http",不是 "streamable-http"。
   Claude Code 用 streamable-http 会静默失败/卡在 connecting 状态。
2. Cursor ~/.cursor/mcp.json:不应带 type 字段——Cursor 按远程 URL 自动
   识别 streamable-http,仅需 url + headers。此前 cursor_json 直接
   clone claude_code_json,继承了错误的 type 字段。

其余配置核实无误:Cline(streamableHttp 驼峰 + disabled/autoApprove)、
Claude Code CLI(--transport http --header)、Oh-My-Pi(transport 字段)。

改动:
- 拆分 cursor_json,不再 clone claude_code_json
- 更新对应 doc 注释(config.rs / mcp_tokens.rs DTO)
- claude_code 测试断言改为 type=="http"
- cursor_json_equals_claude_code_json → cursor_json_has_no_type_field
This commit is contained in:
xfy 2026-07-28 15:10:01 +08:00
parent 78fb51a310
commit 53376a0373
2 changed files with 27 additions and 13 deletions

View File

@ -269,9 +269,9 @@ fn row_to_mcp_token_meta(row: &tokio_postgres::Row) -> McpToken {
/// 为可两端共享的 DTO让 WASM 前端能经 server fn 拿到配置字符串。 /// 为可两端共享的 DTO让 WASM 前端能经 server fn 拿到配置字符串。
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct McpClientConfigs { pub struct McpClientConfigs {
/// Claude Code`.mcp.json` / `~/.claude.json`与 Cursor`~/.cursor/mcp.json`)。 /// Claude Code`.mcp.json` / `~/.claude.json`。`type: "http"`(非 `streamable-http`)。
pub claude_code_json: String, pub claude_code_json: String,
/// Cursor 专用变体(与 claude_code_json 相同,单独列出便于标注) /// Cursor`~/.cursor/mcp.json`)。不带 `type` 字段,按 URL 自动识别 streamable-http
pub cursor_json: String, pub cursor_json: String,
/// Cline`cline_mcp_settings.json`)。 /// Cline`cline_mcp_settings.json`)。
pub cline_json: String, pub cline_json: String,

View File

@ -16,11 +16,12 @@ use serde::Serialize;
/// `token` 形如 `ygg_...`,已嵌入各片段的 `Authorization` 头中。 /// `token` 形如 `ygg_...`,已嵌入各片段的 `Authorization` 头中。
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct ClientConfigs { pub struct ClientConfigs {
/// Claude Code`.mcp.json` / `~/.claude.json`)与 Cursor`~/.cursor/mcp.json`)。 /// Claude Code`.mcp.json` / `~/.claude.json`)。注意 `type` 值是 `"http"`
/// `type: "streamable-http"``url``headers.Authorization`。 ///(不是 `"streamable-http"`——Claude Code 用 `"streamable-http"` 会静默失败/卡在
/// "connecting"2026 官方文档明确要求 `http`)。字段:`type``url``headers.Authorization`。
pub claude_code_json: String, pub claude_code_json: String,
/// Cursor 专用变体(`type: "streamable-http"`)——与 claude_code_json 实际相同, /// Cursor`~/.cursor/mcp.json`)。与 Claude Code 的关键差异:**不带 `type` 字段**——
/// 单独列出便于 UI 分别展示与标注文件路径 /// Cursor 按远程 URL 自动识别 streamable-http仅需 `url` + `headers.Authorization`
pub cursor_json: String, pub cursor_json: String,
/// Cline`cline_mcp_settings.json`)。`type: "streamableHttp"`(注意驼峰,非 `sse` /// Cline`cline_mcp_settings.json`)。`type: "streamableHttp"`(注意驼峰,非 `sse`
/// 额外带 `disabled` / `autoApprove` 字段。 /// 额外带 `disabled` / `autoApprove` 字段。
@ -54,18 +55,26 @@ pub fn generate_client_configs(base_url: &str, token: &str) -> ClientConfigs {
let mcp_url = join_mcp_url(base_url); let mcp_url = join_mcp_url(base_url);
let auth_header = format!("Bearer {token}"); let auth_header = format!("Bearer {token}");
// --- Claude Code / Cursortype = "streamable-http" --- // --- Claude Codetype = "http"(非 "streamable-http",否则静默连接失败) ---
let claude_code_json = serde_json::json!({ let claude_code_json = serde_json::json!({
"mcpServers": { "mcpServers": {
SERVER_NAME: { SERVER_NAME: {
"type": "streamable-http", "type": "http",
"url": mcp_url,
"headers": { "Authorization": auth_header }
}
}
});
// --- Cursor不带 type 字段,按 URL 自动识别 streamable-http ---
let cursor_json = serde_json::json!({
"mcpServers": {
SERVER_NAME: {
"url": mcp_url, "url": mcp_url,
"headers": { "Authorization": auth_header } "headers": { "Authorization": auth_header }
} }
} }
}); });
// Cursor 与 Claude Code 格式一致type 都接受 streamable-http仅展示标签不同。
let cursor_json = claude_code_json.clone();
// --- Clinetype = "streamableHttp"(驼峰),带 disabled / autoApprove --- // --- Clinetype = "streamableHttp"(驼峰),带 disabled / autoApprove ---
let cline_json = serde_json::json!({ let cline_json = serde_json::json!({
@ -149,14 +158,19 @@ mod tests {
v["mcpServers"]["yggdrasil"]["headers"]["Authorization"], v["mcpServers"]["yggdrasil"]["headers"]["Authorization"],
format!("Bearer {TOKEN}") format!("Bearer {TOKEN}")
); );
assert_eq!(v["mcpServers"]["yggdrasil"]["type"], "streamable-http"); assert_eq!(v["mcpServers"]["yggdrasil"]["type"], "http"); // 非 "streamable-http"(会静默失败)
assert_eq!(v["mcpServers"]["yggdrasil"]["url"], "https://rua.plus/mcp"); assert_eq!(v["mcpServers"]["yggdrasil"]["url"], "https://rua.plus/mcp");
} }
#[test] #[test]
fn cursor_json_equals_claude_code_json() { fn cursor_json_has_no_type_field() {
let cfg = generate_client_configs(BASE, TOKEN); let cfg = generate_client_configs(BASE, TOKEN);
assert_eq!(cfg.cursor_json, cfg.claude_code_json); let v: serde_json::Value = serde_json::from_str(&cfg.cursor_json).unwrap();
let entry = &v["mcpServers"]["yggdrasil"];
// Cursor 按 URL 自动识别远程端点,不带 type 字段。
assert!(entry.get("type").is_none(), "cursor 配置不应含 type 字段");
assert_eq!(entry["url"], "https://rua.plus/mcp");
assert_eq!(entry["headers"]["Authorization"], format!("Bearer {TOKEN}"));
} }
#[test] #[test]