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:
@@ -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 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_stdio = t_opt.map(|t| t == "stdio").unwrap_or(true); // 兼容缺省(按 stdio 处理)
|
||||||
let is_http = t_opt.map(|t| t == "http").unwrap_or(false);
|
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(
|
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
|
// http/sse 类型必须有 url
|
||||||
if is_http {
|
if is_http || is_sse {
|
||||||
let url = spec.get("url").and_then(|x| x.as_str()).unwrap_or("");
|
let url = spec.get("url").and_then(|x| x.as_str()).unwrap_or("");
|
||||||
if url.is_empty() {
|
if url.is_empty() {
|
||||||
return Err(AppError::McpValidation(
|
return Err(AppError::McpValidation(
|
||||||
"http 类型的 MCP 服务器缺少 url 字段".into(),
|
if is_http {
|
||||||
|
"http 类型的 MCP 服务器缺少 url 字段".into()
|
||||||
|
} else {
|
||||||
|
"sse 类型的 MCP 服务器缺少 url 字段".into()
|
||||||
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::collections::HashMap;
|
|||||||
use crate::app_config::{AppType, McpConfig, MultiAppConfig};
|
use crate::app_config::{AppType, McpConfig, MultiAppConfig};
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
|
|
||||||
/// 基础校验:允许 stdio/http;或省略 type(视为 stdio)。对应必填字段存在
|
/// 基础校验:允许 stdio/http/sse;或省略 type(视为 stdio)。对应必填字段存在
|
||||||
fn validate_server_spec(spec: &Value) -> Result<(), AppError> {
|
fn validate_server_spec(spec: &Value) -> Result<(), AppError> {
|
||||||
if !spec.is_object() {
|
if !spec.is_object() {
|
||||||
return Err(AppError::McpValidation(
|
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());
|
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_stdio = t_opt.map(|t| t == "stdio").unwrap_or(true);
|
||||||
let is_http = t_opt.map(|t| t == "http").unwrap_or(false);
|
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(
|
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(())
|
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()) {
|
if let Some(url) = entry_tbl.get("url").and_then(|v| v.as_str()) {
|
||||||
spec.insert("url".into(), json!(url));
|
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("");
|
let url = spec.get("url").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
t["url"] = toml_edit::value(url);
|
t["url"] = toml_edit::value(url);
|
||||||
if let Some(headers) = spec.get("headers").and_then(|v| v.as_object()) {
|
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("");
|
let url = spec.get("url").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
t["url"] = toml_edit::value(url);
|
t["url"] = toml_edit::value(url);
|
||||||
|
|
||||||
|
|||||||
@@ -346,7 +346,10 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
|
|||||||
toast.error(t("mcp.error.commandRequired"), { duration: 3000 });
|
toast.error(t("mcp.error.commandRequired"), { duration: 3000 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (serverSpec?.type === "http" && !serverSpec?.url?.trim()) {
|
if (
|
||||||
|
(serverSpec?.type === "http" || serverSpec?.type === "sse") &&
|
||||||
|
!serverSpec?.url?.trim()
|
||||||
|
) {
|
||||||
toast.error(t("mcp.wizard.urlRequired"), { duration: 3000 });
|
toast.error(t("mcp.wizard.urlRequired"), { duration: 3000 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,13 +80,13 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
|||||||
initialServer,
|
initialServer,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [wizardType, setWizardType] = useState<"stdio" | "http">("stdio");
|
const [wizardType, setWizardType] = useState<"stdio" | "http" | "sse">("stdio");
|
||||||
const [wizardTitle, setWizardTitle] = useState("");
|
const [wizardTitle, setWizardTitle] = useState("");
|
||||||
// stdio 字段
|
// stdio 字段
|
||||||
const [wizardCommand, setWizardCommand] = useState("");
|
const [wizardCommand, setWizardCommand] = useState("");
|
||||||
const [wizardArgs, setWizardArgs] = useState("");
|
const [wizardArgs, setWizardArgs] = useState("");
|
||||||
const [wizardEnv, setWizardEnv] = useState("");
|
const [wizardEnv, setWizardEnv] = useState("");
|
||||||
// http 字段
|
// http 和 sse 字段
|
||||||
const [wizardUrl, setWizardUrl] = useState("");
|
const [wizardUrl, setWizardUrl] = useState("");
|
||||||
const [wizardHeaders, setWizardHeaders] = useState("");
|
const [wizardHeaders, setWizardHeaders] = useState("");
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// http 类型必需字段
|
// http 和 sse 类型必需字段
|
||||||
config.url = wizardUrl.trim();
|
config.url = wizardUrl.trim();
|
||||||
|
|
||||||
// 可选字段
|
// 可选字段
|
||||||
@@ -139,7 +139,7 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
|||||||
toast.error(t("mcp.error.commandRequired"), { duration: 3000 });
|
toast.error(t("mcp.error.commandRequired"), { duration: 3000 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (wizardType === "http" && !wizardUrl.trim()) {
|
if ((wizardType === "http" || wizardType === "sse") && !wizardUrl.trim()) {
|
||||||
toast.error(t("mcp.wizard.urlRequired"), { duration: 3000 });
|
toast.error(t("mcp.wizard.urlRequired"), { duration: 3000 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,7 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
|||||||
|
|
||||||
setWizardType(resolvedType);
|
setWizardType(resolvedType);
|
||||||
|
|
||||||
if (resolvedType === "http") {
|
if (resolvedType === "http" || resolvedType === "sse") {
|
||||||
setWizardUrl(initialServer?.url ?? "");
|
setWizardUrl(initialServer?.url ?? "");
|
||||||
const headersCandidate = initialServer?.headers;
|
const headersCandidate = initialServer?.headers;
|
||||||
const headers =
|
const headers =
|
||||||
@@ -250,7 +250,7 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
|||||||
value="stdio"
|
value="stdio"
|
||||||
checked={wizardType === "stdio"}
|
checked={wizardType === "stdio"}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setWizardType(e.target.value as "stdio" | "http")
|
setWizardType(e.target.value as "stdio" | "http" | "sse")
|
||||||
}
|
}
|
||||||
className="w-4 h-4 text-emerald-500 bg-white dark:bg-gray-800 border-border-default focus:ring-emerald-500 dark:focus:ring-emerald-400 focus:ring-2"
|
className="w-4 h-4 text-emerald-500 bg-white dark:bg-gray-800 border-border-default focus:ring-emerald-500 dark:focus:ring-emerald-400 focus:ring-2"
|
||||||
/>
|
/>
|
||||||
@@ -264,7 +264,7 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
|||||||
value="http"
|
value="http"
|
||||||
checked={wizardType === "http"}
|
checked={wizardType === "http"}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setWizardType(e.target.value as "stdio" | "http")
|
setWizardType(e.target.value as "stdio" | "http" | "sse")
|
||||||
}
|
}
|
||||||
className="w-4 h-4 text-emerald-500 bg-white dark:bg-gray-800 border-border-default focus:ring-emerald-500 dark:focus:ring-emerald-400 focus:ring-2"
|
className="w-4 h-4 text-emerald-500 bg-white dark:bg-gray-800 border-border-default focus:ring-emerald-500 dark:focus:ring-emerald-400 focus:ring-2"
|
||||||
/>
|
/>
|
||||||
@@ -272,6 +272,20 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
|||||||
{t("mcp.wizard.typeHttp")}
|
{t("mcp.wizard.typeHttp")}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="inline-flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value="sse"
|
||||||
|
checked={wizardType === "sse"}
|
||||||
|
onChange={(e) =>
|
||||||
|
setWizardType(e.target.value as "stdio" | "http" | "sse")
|
||||||
|
}
|
||||||
|
className="w-4 h-4 text-emerald-500 bg-white dark:bg-gray-800 border-border-default focus:ring-emerald-500 dark:focus:ring-emerald-400 focus:ring-2"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-900 dark:text-gray-100">
|
||||||
|
{t("mcp.wizard.typeSse")}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -339,8 +353,8 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* HTTP 类型字段 */}
|
{/* HTTP 和 SSE 类型字段 */}
|
||||||
{wizardType === "http" && (
|
{(wizardType === "http" || wizardType === "sse") && (
|
||||||
<>
|
<>
|
||||||
{/* URL */}
|
{/* URL */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ export function useMcpValidation() {
|
|||||||
if (server.type === "stdio" && !server.command?.trim()) {
|
if (server.type === "stdio" && !server.command?.trim()) {
|
||||||
return t("mcp.error.commandRequired");
|
return t("mcp.error.commandRequired");
|
||||||
}
|
}
|
||||||
if (server.type === "http" && !server.url?.trim()) {
|
if (
|
||||||
|
(server.type === "http" || server.type === "sse") &&
|
||||||
|
!server.url?.trim()
|
||||||
|
) {
|
||||||
return t("mcp.wizard.urlRequired");
|
return t("mcp.wizard.urlRequired");
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -73,7 +76,10 @@ export function useMcpValidation() {
|
|||||||
if (typ === "stdio" && !(obj as any)?.command?.trim()) {
|
if (typ === "stdio" && !(obj as any)?.command?.trim()) {
|
||||||
return t("mcp.error.commandRequired");
|
return t("mcp.error.commandRequired");
|
||||||
}
|
}
|
||||||
if (typ === "http" && !(obj as any)?.url?.trim()) {
|
if (
|
||||||
|
(typ === "http" || typ === "sse") &&
|
||||||
|
!(obj as any)?.url?.trim()
|
||||||
|
) {
|
||||||
return t("mcp.wizard.urlRequired");
|
return t("mcp.wizard.urlRequired");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -483,6 +483,7 @@
|
|||||||
"type": "Type",
|
"type": "Type",
|
||||||
"typeStdio": "stdio",
|
"typeStdio": "stdio",
|
||||||
"typeHttp": "http",
|
"typeHttp": "http",
|
||||||
|
"typeSse": "sse",
|
||||||
"command": "Command",
|
"command": "Command",
|
||||||
"commandPlaceholder": "npx or uvx",
|
"commandPlaceholder": "npx or uvx",
|
||||||
"args": "Arguments",
|
"args": "Arguments",
|
||||||
|
|||||||
@@ -483,6 +483,7 @@
|
|||||||
"type": "类型",
|
"type": "类型",
|
||||||
"typeStdio": "stdio",
|
"typeStdio": "stdio",
|
||||||
"typeHttp": "http",
|
"typeHttp": "http",
|
||||||
|
"typeSse": "sse",
|
||||||
"command": "命令",
|
"command": "命令",
|
||||||
"commandPlaceholder": "npx 或 uvx",
|
"commandPlaceholder": "npx 或 uvx",
|
||||||
"args": "参数",
|
"args": "参数",
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const jsonConfigSchema = z
|
|||||||
* 通用的 TOML 配置文本校验:
|
* 通用的 TOML 配置文本校验:
|
||||||
* - 允许为空(由上层业务决定是否必填)
|
* - 允许为空(由上层业务决定是否必填)
|
||||||
* - 语法与结构有效
|
* - 语法与结构有效
|
||||||
* - 针对 stdio/http 的必填字段(command/url)进行提示
|
* - 针对 stdio/http/sse 的必填字段(command/url)进行提示
|
||||||
*/
|
*/
|
||||||
export const tomlConfigSchema = z.string().superRefine((value, ctx) => {
|
export const tomlConfigSchema = z.string().superRefine((value, ctx) => {
|
||||||
const err = validateToml(value);
|
const err = validateToml(value);
|
||||||
@@ -80,10 +80,13 @@ export const tomlConfigSchema = z.string().superRefine((value, ctx) => {
|
|||||||
message: "stdio 类型需填写 command",
|
message: "stdio 类型需填写 command",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (server.type === "http" && !server.url?.trim()) {
|
if (
|
||||||
|
(server.type === "http" || server.type === "sse") &&
|
||||||
|
!server.url?.trim()
|
||||||
|
) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message: "http 类型需填写 url",
|
message: `${server.type} 类型需填写 url`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const mcpServerSpecSchema = z
|
const mcpServerSpecSchema = z
|
||||||
.object({
|
.object({
|
||||||
type: z.enum(["stdio", "http"]).optional(),
|
type: z.enum(["stdio", "http", "sse"]).optional(),
|
||||||
command: z.string().trim().optional(),
|
command: z.string().trim().optional(),
|
||||||
args: z.array(z.string()).optional(),
|
args: z.array(z.string()).optional(),
|
||||||
env: z.record(z.string(), z.string()).optional(),
|
env: z.record(z.string(), z.string()).optional(),
|
||||||
@@ -19,10 +19,10 @@ const mcpServerSpecSchema = z
|
|||||||
path: ["command"],
|
path: ["command"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (type === "http" && !server.url?.trim()) {
|
if ((type === "http" || type === "sse") && !server.url?.trim()) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message: "http 类型需填写 url",
|
message: `${type} 类型需填写 url`,
|
||||||
path: ["url"],
|
path: ["url"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,13 +112,13 @@ export interface Settings {
|
|||||||
// MCP 服务器连接参数(宽松:允许扩展字段)
|
// MCP 服务器连接参数(宽松:允许扩展字段)
|
||||||
export interface McpServerSpec {
|
export interface McpServerSpec {
|
||||||
// 可选:社区常见 .mcp.json 中 stdio 配置可不写 type
|
// 可选:社区常见 .mcp.json 中 stdio 配置可不写 type
|
||||||
type?: "stdio" | "http";
|
type?: "stdio" | "http" | "sse";
|
||||||
// stdio 字段
|
// stdio 字段
|
||||||
command?: string;
|
command?: string;
|
||||||
args?: string[];
|
args?: string[];
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
// http 字段
|
// http 和 sse 字段
|
||||||
url?: string;
|
url?: string;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
// 通用字段
|
// 通用字段
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export const translateMcpBackendError = (
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
msg.includes("http 类型的 MCP 服务器缺少 url 字段") ||
|
msg.includes("http 类型的 MCP 服务器缺少 url 字段") ||
|
||||||
|
msg.includes("sse 类型的 MCP 服务器缺少 url 字段") ||
|
||||||
msg.includes("必须包含 url 字段") ||
|
msg.includes("必须包含 url 字段") ||
|
||||||
msg === "URL 不能为空"
|
msg === "URL 不能为空"
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -145,13 +145,13 @@ function normalizeServerConfig(config: any): McpServerSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return server;
|
return server;
|
||||||
} else if (type === "http") {
|
} else if (type === "http" || type === "sse") {
|
||||||
if (!config.url || typeof config.url !== "string") {
|
if (!config.url || typeof config.url !== "string") {
|
||||||
throw new Error("http 类型的 MCP 服务器必须包含 url 字段");
|
throw new Error(`${type} 类型的 MCP 服务器必须包含 url 字段`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const server: McpServerSpec = {
|
const server: McpServerSpec = {
|
||||||
type: "http",
|
type: type as "http" | "sse",
|
||||||
url: config.url,
|
url: config.url,
|
||||||
};
|
};
|
||||||
knownFields.add("type");
|
knownFields.add("type");
|
||||||
|
|||||||
@@ -87,6 +87,15 @@ describe("useMcpValidation", () => {
|
|||||||
expect(validateTomlConfig("foo")).toBe("mcp.wizard.urlRequired");
|
expect(validateTomlConfig("foo")).toBe("mcp.wizard.urlRequired");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns url required when sse server missing url", () => {
|
||||||
|
tomlToMcpServerMock.mockReturnValue({
|
||||||
|
type: "sse",
|
||||||
|
url: "",
|
||||||
|
});
|
||||||
|
const { validateTomlConfig } = getHookResult();
|
||||||
|
expect(validateTomlConfig("foo")).toBe("mcp.wizard.urlRequired");
|
||||||
|
});
|
||||||
|
|
||||||
it("surface tomlToMcpServer errors via formatter", () => {
|
it("surface tomlToMcpServer errors via formatter", () => {
|
||||||
tomlToMcpServerMock.mockImplementation(() => {
|
tomlToMcpServerMock.mockImplementation(() => {
|
||||||
throw new Error("normalize failed");
|
throw new Error("normalize failed");
|
||||||
@@ -128,6 +137,11 @@ describe("useMcpValidation", () => {
|
|||||||
expect(validateJsonConfig('{"type":"http","url":""}')).toBe("mcp.wizard.urlRequired");
|
expect(validateJsonConfig('{"type":"http","url":""}')).toBe("mcp.wizard.urlRequired");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("requires url for sse type", () => {
|
||||||
|
const { validateJsonConfig } = getHookResult();
|
||||||
|
expect(validateJsonConfig('{"type":"sse","url":""}')).toBe("mcp.wizard.urlRequired");
|
||||||
|
});
|
||||||
|
|
||||||
it("returns empty string when json config valid", () => {
|
it("returns empty string when json config valid", () => {
|
||||||
const { validateJsonConfig } = getHookResult();
|
const { validateJsonConfig } = getHookResult();
|
||||||
expect(
|
expect(
|
||||||
@@ -142,4 +156,3 @@ describe("useMcpValidation", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user