Bot commited on
Commit
01f3995
·
1 Parent(s): 3011824

Support standard mcpServers configuration batch import

Browse files
Files changed (1) hide show
  1. src/components/mcp-editor.tsx +92 -8
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">