Spaces:
Sleeping
Sleeping
Bot commited on
Commit ·
01f3995
1
Parent(s): 3011824
Support standard mcpServers configuration batch import
Browse files
src/components/mcp-editor.tsx
CHANGED
|
@@ -20,6 +20,7 @@ import { Loader } from "lucide-react";
|
|
| 20 |
import {
|
| 21 |
isMaybeMCPServerConfig,
|
| 22 |
isMaybeRemoteConfig,
|
|
|
|
| 23 |
} from "lib/ai/mcp/is-mcp-config";
|
| 24 |
|
| 25 |
import { Alert, AlertDescription, AlertTitle } from "ui/alert";
|
|
@@ -48,8 +49,36 @@ const STDIO_ARGS_ENV_PLACEHOLDER = `/** STDIO Example */
|
|
| 48 |
"headers": {
|
| 49 |
"Authorization": "Bearer sk-..."
|
| 50 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}`;
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
export default function MCPEditor({
|
| 54 |
initialConfig,
|
| 55 |
name: initialName,
|
|
@@ -91,18 +120,24 @@ export default function MCPEditor({
|
|
| 91 |
return true;
|
| 92 |
};
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
const saveDisabled = useMemo(() => {
|
| 95 |
return (
|
| 96 |
-
name.trim() === "" ||
|
| 97 |
isLoading ||
|
| 98 |
!!jsonError ||
|
| 99 |
-
!!nameError ||
|
| 100 |
-
!isMaybeMCPServerConfig(config)
|
| 101 |
);
|
| 102 |
-
}, [isLoading, jsonError, nameError, config, name]);
|
| 103 |
|
| 104 |
// Validate
|
| 105 |
const validateConfig = (jsonConfig: unknown): boolean => {
|
|
|
|
|
|
|
| 106 |
const result = isMaybeRemoteConfig(jsonConfig)
|
| 107 |
? MCPRemoteConfigZodSchema.safeParse(jsonConfig)
|
| 108 |
: MCPStdioConfigZodSchema.safeParse(jsonConfig);
|
|
@@ -114,6 +149,55 @@ export default function MCPEditor({
|
|
| 114 |
|
| 115 |
// Handle save button click
|
| 116 |
const handleSave = async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
// Perform validation
|
| 118 |
if (!validateConfig(config)) return;
|
| 119 |
if (!name) {
|
|
@@ -184,16 +268,16 @@ export default function MCPEditor({
|
|
| 184 |
|
| 185 |
<Input
|
| 186 |
id="name"
|
| 187 |
-
value={name}
|
| 188 |
-
disabled={!shouldInsert}
|
| 189 |
onChange={(e) => {
|
| 190 |
setName(e.target.value);
|
| 191 |
if (e.target.value) validateName(e.target.value);
|
| 192 |
}}
|
| 193 |
placeholder={t("MCP.enterMcpServerName")}
|
| 194 |
-
className={nameError ? "border-destructive" : ""}
|
| 195 |
/>
|
| 196 |
-
{nameError && <p className="text-xs text-destructive">{nameError}</p>}
|
| 197 |
</div>
|
| 198 |
<div className="space-y-4">
|
| 199 |
<div className="space-y-2">
|
|
|
|
| 20 |
import {
|
| 21 |
isMaybeMCPServerConfig,
|
| 22 |
isMaybeRemoteConfig,
|
| 23 |
+
isMaybeStdioConfig,
|
| 24 |
} from "lib/ai/mcp/is-mcp-config";
|
| 25 |
|
| 26 |
import { Alert, AlertDescription, AlertTitle } from "ui/alert";
|
|
|
|
| 49 |
"headers": {
|
| 50 |
"Authorization": "Bearer sk-..."
|
| 51 |
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/** Standard mcpServers JSON Example */
|
| 55 |
+
{
|
| 56 |
+
"mcpServers": {
|
| 57 |
+
"my-server": {
|
| 58 |
+
"command": "node",
|
| 59 |
+
"args": ["index.js"]
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
}`;
|
| 63 |
|
| 64 |
+
// Helper to check and extract standard mcpServers configurations
|
| 65 |
+
const getMcpServersFromObject = (obj: any): Record<string, MCPServerConfig> | null => {
|
| 66 |
+
if (!obj || typeof obj !== "object") return null;
|
| 67 |
+
const servers = obj.mcpServers || obj;
|
| 68 |
+
if (typeof servers !== "object" || Array.isArray(servers)) return null;
|
| 69 |
+
|
| 70 |
+
const keys = Object.keys(servers);
|
| 71 |
+
if (keys.length === 0) return null;
|
| 72 |
+
|
| 73 |
+
// Verify all keys represent valid server configs
|
| 74 |
+
const allValid = keys.every(key => {
|
| 75 |
+
const srv = servers[key];
|
| 76 |
+
return srv && typeof srv === "object" && isMaybeMCPServerConfig(srv);
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
return allValid ? servers : null;
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
export default function MCPEditor({
|
| 83 |
initialConfig,
|
| 84 |
name: initialName,
|
|
|
|
| 120 |
return true;
|
| 121 |
};
|
| 122 |
|
| 123 |
+
const isMulti = useMemo(() => {
|
| 124 |
+
return getMcpServersFromObject(config) !== null;
|
| 125 |
+
}, [config]);
|
| 126 |
+
|
| 127 |
const saveDisabled = useMemo(() => {
|
| 128 |
return (
|
| 129 |
+
(!isMulti && name.trim() === "") ||
|
| 130 |
isLoading ||
|
| 131 |
!!jsonError ||
|
| 132 |
+
(!isMulti && !!nameError) ||
|
| 133 |
+
!(isMaybeMCPServerConfig(config) || isMulti)
|
| 134 |
);
|
| 135 |
+
}, [isLoading, jsonError, nameError, config, name, isMulti]);
|
| 136 |
|
| 137 |
// Validate
|
| 138 |
const validateConfig = (jsonConfig: unknown): boolean => {
|
| 139 |
+
const isMultiConfig = getMcpServersFromObject(jsonConfig) !== null;
|
| 140 |
+
if (isMultiConfig) return true;
|
| 141 |
const result = isMaybeRemoteConfig(jsonConfig)
|
| 142 |
? MCPRemoteConfigZodSchema.safeParse(jsonConfig)
|
| 143 |
: MCPStdioConfigZodSchema.safeParse(jsonConfig);
|
|
|
|
| 149 |
|
| 150 |
// Handle save button click
|
| 151 |
const handleSave = async () => {
|
| 152 |
+
const mcpServers = getMcpServersFromObject(config);
|
| 153 |
+
if (mcpServers) {
|
| 154 |
+
setIsLoading(true);
|
| 155 |
+
try {
|
| 156 |
+
for (const [srvName, srvConfig] of Object.entries(mcpServers)) {
|
| 157 |
+
// Validate name
|
| 158 |
+
const nameResult = nameSchema.safeParse(srvName);
|
| 159 |
+
if (!nameResult.success) {
|
| 160 |
+
throw new Error(`Invalid server name "${srvName}": ${nameResult.error.issues[0].message}`);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
// Validate individual config schema
|
| 164 |
+
const configResult = isMaybeRemoteConfig(srvConfig)
|
| 165 |
+
? MCPRemoteConfigZodSchema.safeParse(srvConfig)
|
| 166 |
+
: MCPStdioConfigZodSchema.safeParse(srvConfig);
|
| 167 |
+
if (!configResult.success) {
|
| 168 |
+
throw new Error(`Invalid config for server "${srvName}": ${configResult.error.issues[0].message}`);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
if (shouldInsert) {
|
| 172 |
+
const exist = await existMcpClientByServerNameAction(srvName);
|
| 173 |
+
if (exist) {
|
| 174 |
+
throw new Error(`Server "${srvName}" already exists`);
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// Save each server
|
| 180 |
+
for (const [srvName, srvConfig] of Object.entries(mcpServers)) {
|
| 181 |
+
await fetcher("/api/mcp", {
|
| 182 |
+
method: "POST",
|
| 183 |
+
body: JSON.stringify({
|
| 184 |
+
name: srvName,
|
| 185 |
+
config: srvConfig,
|
| 186 |
+
}),
|
| 187 |
+
});
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
toast.success("All MCP configurations saved successfully");
|
| 191 |
+
mutate("/api/mcp/list");
|
| 192 |
+
router.push("/mcp");
|
| 193 |
+
} catch (err: any) {
|
| 194 |
+
handleErrorWithToast(err);
|
| 195 |
+
} finally {
|
| 196 |
+
setIsLoading(false);
|
| 197 |
+
}
|
| 198 |
+
return;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
// Perform validation
|
| 202 |
if (!validateConfig(config)) return;
|
| 203 |
if (!name) {
|
|
|
|
| 268 |
|
| 269 |
<Input
|
| 270 |
id="name"
|
| 271 |
+
value={isMulti ? "Multiple Servers Detected (Names read from JSON)" : name}
|
| 272 |
+
disabled={!shouldInsert || isMulti}
|
| 273 |
onChange={(e) => {
|
| 274 |
setName(e.target.value);
|
| 275 |
if (e.target.value) validateName(e.target.value);
|
| 276 |
}}
|
| 277 |
placeholder={t("MCP.enterMcpServerName")}
|
| 278 |
+
className={nameError && !isMulti ? "border-destructive" : ""}
|
| 279 |
/>
|
| 280 |
+
{nameError && !isMulti && <p className="text-xs text-destructive">{nameError}</p>}
|
| 281 |
</div>
|
| 282 |
<div className="space-y-4">
|
| 283 |
<div className="space-y-2">
|