feat(mcp): add SSE (Server-Sent Events) transport type support

Add comprehensive support for SSE transport type to MCP server configuration,
enabling real-time streaming connections alongside existing stdio and http types.

Backend Changes:
- Add SSE type validation in mcp.rs validate_server_spec()
- Extend Codex TOML import/export to handle SSE servers
- Update claude_mcp.rs legacy API for backward compatibility
- Unify http/sse handling in json_server_to_toml_table()

Frontend Changes:
- Extend McpServerSpec type definition to include "sse"
- Add SSE radio button to configuration wizard UI
- Update wizard form logic to handle SSE url and headers
- Add SSE validation in McpFormModal submission

Validation & Error Handling:
- Add SSE support in useMcpValidation hook (TOML/JSON)
- Extend tomlUtils normalizeServerConfig for SSE parsing
- Update Zod schemas (common.ts, mcp.ts) with SSE enum
- Add SSE error message mapping in errorUtils

Internationalization:
- Add "typeSse" translations (zh: "sse", en: "sse")

Tests:
- Add SSE validation test cases in useMcpValidation.test.tsx

SSE Configuration Format:
{
  "type": "sse",
  "url": "https://api.example.com/sse",
  "headers": { "Authorization": "Bearer token" }
}
This commit is contained in:
Jason
2025-11-16 16:15:17 +08:00
parent 4fc7413ffa
commit bfc27349b3
13 changed files with 92 additions and 36 deletions

View File

@@ -118,9 +118,10 @@ pub fn upsert_mcp_server(id: &str, spec: Value) -> Result<bool, AppError> {
let t_opt = spec.get("type").and_then(|x| x.as_str());
let is_stdio = t_opt.map(|t| t == "stdio").unwrap_or(true); // 兼容缺省(按 stdio 处理)
let is_http = t_opt.map(|t| t == "http").unwrap_or(false);
if !(is_stdio || is_http) {
let is_sse = t_opt.map(|t| t == "sse").unwrap_or(false);
if !(is_stdio || is_http || is_sse) {
return Err(AppError::McpValidation(
"MCP 服务器 type 必须是 'stdio''http'(或省略表示 stdio".into(),
"MCP 服务器 type 必须是 'stdio''http' 或 'sse'(或省略表示 stdio".into(),
));
}
@@ -134,12 +135,16 @@ pub fn upsert_mcp_server(id: &str, spec: Value) -> Result<bool, AppError> {
}
}
// http 类型必须有 url
if is_http {
// http/sse 类型必须有 url
if is_http || is_sse {
let url = spec.get("url").and_then(|x| x.as_str()).unwrap_or("");
if url.is_empty() {
return Err(AppError::McpValidation(
"http 类型的 MCP 服务器缺少 url 字段".into(),
if is_http {
"http 类型的 MCP 服务器缺少 url 字段".into()
} else {
"sse 类型的 MCP 服务器缺少 url 字段".into()
},
));
}
}

View File

@@ -4,7 +4,7 @@ use std::collections::HashMap;
use crate::app_config::{AppType, McpConfig, MultiAppConfig};
use crate::error::AppError;
/// 基础校验:允许 stdio/http或省略 type视为 stdio。对应必填字段存在
/// 基础校验:允许 stdio/http/sse;或省略 type视为 stdio。对应必填字段存在
fn validate_server_spec(spec: &Value) -> Result<(), AppError> {
if !spec.is_object() {
return Err(AppError::McpValidation(
@@ -12,13 +12,14 @@ fn validate_server_spec(spec: &Value) -> Result<(), AppError> {
));
}
let t_opt = spec.get("type").and_then(|x| x.as_str());
// 支持stdio/http若缺省 type 则按 stdio 处理(与社区常见 .mcp.json 一致)
// 支持stdio/http/sse;若缺省 type 则按 stdio 处理(与社区常见 .mcp.json 一致)
let is_stdio = t_opt.map(|t| t == "stdio").unwrap_or(true);
let is_http = t_opt.map(|t| t == "http").unwrap_or(false);
let is_sse = t_opt.map(|t| t == "sse").unwrap_or(false);
if !(is_stdio || is_http) {
if !(is_stdio || is_http || is_sse) {
return Err(AppError::McpValidation(
"MCP 服务器 type 必须是 'stdio''http'(或省略表示 stdio".into(),
"MCP 服务器 type 必须是 'stdio''http' 或 'sse'(或省略表示 stdio".into(),
));
}
@@ -38,6 +39,14 @@ fn validate_server_spec(spec: &Value) -> Result<(), AppError> {
));
}
}
if is_sse {
let url = spec.get("url").and_then(|x| x.as_str()).unwrap_or("");
if url.trim().is_empty() {
return Err(AppError::McpValidation(
"sse 类型的 MCP 服务器缺少 url 字段".into(),
));
}
}
Ok(())
}
@@ -469,7 +478,7 @@ pub fn import_from_codex(config: &mut MultiAppConfig) -> Result<usize, AppError>
}
}
}
"http" => {
"http" | "sse" => {
if let Some(url) = entry_tbl.get("url").and_then(|v| v.as_str()) {
spec.insert("url".into(), json!(url));
}
@@ -643,7 +652,7 @@ pub fn sync_enabled_to_codex(config: &MultiAppConfig) -> Result<(), AppError> {
}
}
}
"http" => {
"http" | "sse" => {
let url = spec.get("url").and_then(|v| v.as_str()).unwrap_or("");
t["url"] = toml_edit::value(url);
if let Some(headers) = spec.get("headers").and_then(|v| v.as_object()) {
@@ -858,7 +867,7 @@ fn json_server_to_toml_table(spec: &Value) -> Result<toml_edit::Table, AppError>
}
}
}
"http" => {
"http" | "sse" => {
let url = spec.get("url").and_then(|v| v.as_str()).unwrap_or("");
t["url"] = toml_edit::value(url);