Skip to content

Commit 75984fc

Browse files
committed
🤖 feat: add MCP server edit functionality
- Add /mcp edit <name> <command> slash command - Add inline edit button in Settings UI - Enter to save, Esc to cancel
1 parent f5e6fb5 commit 75984fc

File tree

4 files changed

+155
-32
lines changed

4 files changed

+155
-32
lines changed

src/browser/components/ChatInput/index.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,11 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
740740
return;
741741
}
742742

743-
if (parsed.type === "mcp-add" || parsed.type === "mcp-remove") {
743+
if (
744+
parsed.type === "mcp-add" ||
745+
parsed.type === "mcp-edit" ||
746+
parsed.type === "mcp-remove"
747+
) {
744748
if (!api) {
745749
setToast({
746750
id: Date.now().toString(),
@@ -763,7 +767,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
763767
try {
764768
const projectPath = selectedWorkspace.projectPath;
765769
const result =
766-
parsed.type === "mcp-add"
770+
parsed.type === "mcp-add" || parsed.type === "mcp-edit"
767771
? await api.projects.mcp.add({
768772
projectPath,
769773
name: parsed.name,
@@ -779,13 +783,16 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
779783
});
780784
setInput(messageText);
781785
} else {
786+
const successMessage =
787+
parsed.type === "mcp-add"
788+
? `Added MCP server ${parsed.name}`
789+
: parsed.type === "mcp-edit"
790+
? `Updated MCP server ${parsed.name}`
791+
: `Removed MCP server ${parsed.name}`;
782792
setToast({
783793
id: Date.now().toString(),
784794
type: "success",
785-
message:
786-
parsed.type === "mcp-add"
787-
? `Added MCP server ${parsed.name}`
788-
: `Removed MCP server ${parsed.name}`,
795+
message: successMessage,
789796
});
790797
}
791798
} catch (error) {

src/browser/components/Settings/sections/ProjectSettingsSection.tsx

Lines changed: 129 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import {
1010
Plus,
1111
ChevronDown,
1212
Server,
13+
Pencil,
14+
Check,
15+
X,
1316
} from "lucide-react";
1417

1518
type TestResult = { success: true; tools: string[] } | { success: false; error: string };
@@ -33,6 +36,11 @@ export const ProjectSettingsSection: React.FC = () => {
3336
const [testingNewCommand, setTestingNewCommand] = useState(false);
3437
const [newCommandTestResult, setNewCommandTestResult] = useState<TestResult | null>(null);
3538

39+
// Edit server state
40+
const [editingServer, setEditingServer] = useState<string | null>(null);
41+
const [editCommand, setEditCommand] = useState("");
42+
const [savingEdit, setSavingEdit] = useState(false);
43+
3644
// Set default project when projects load
3745
useEffect(() => {
3846
if (projectList.length > 0 && !selectedProject) {
@@ -155,6 +163,46 @@ export const ProjectSettingsSection: React.FC = () => {
155163
}
156164
}, [api, selectedProject, newServerName, newServerCommand, refresh]);
157165

166+
const handleStartEdit = useCallback((name: string, command: string) => {
167+
setEditingServer(name);
168+
setEditCommand(command);
169+
}, []);
170+
171+
const handleCancelEdit = useCallback(() => {
172+
setEditingServer(null);
173+
setEditCommand("");
174+
}, []);
175+
176+
const handleSaveEdit = useCallback(async () => {
177+
if (!api || !selectedProject || !editingServer || !editCommand.trim()) return;
178+
setSavingEdit(true);
179+
setError(null);
180+
try {
181+
const result = await api.projects.mcp.add({
182+
projectPath: selectedProject,
183+
name: editingServer,
184+
command: editCommand.trim(),
185+
});
186+
if (!result.success) {
187+
setError(result.error ?? "Failed to update MCP server");
188+
} else {
189+
setEditingServer(null);
190+
setEditCommand("");
191+
// Clear test result for this server since command changed
192+
setTestResults((prev) => {
193+
const next = new Map(prev);
194+
next.delete(editingServer);
195+
return next;
196+
});
197+
await refresh();
198+
}
199+
} catch (err) {
200+
setError(err instanceof Error ? err.message : "Failed to update MCP server");
201+
} finally {
202+
setSavingEdit(false);
203+
}
204+
}, [api, selectedProject, editingServer, editCommand, refresh]);
205+
158206
if (projectList.length === 0) {
159207
return (
160208
<div className="flex flex-col items-center justify-center py-12 text-center">
@@ -227,52 +275,107 @@ export const ProjectSettingsSection: React.FC = () => {
227275
{Object.entries(servers).map(([name, command]) => {
228276
const isTesting = testingServer === name;
229277
const testResult = testResults.get(name);
278+
const isEditing = editingServer === name;
230279
return (
231280
<li key={name} className="border-border-medium bg-secondary/20 rounded-lg border p-3">
232281
<div className="flex items-start justify-between gap-3">
233282
<div className="min-w-0 flex-1">
234283
<div className="flex items-center gap-2">
235284
<span className="font-medium">{name}</span>
236-
{testResult?.success && (
285+
{testResult?.success && !isEditing && (
237286
<span className="rounded bg-green-500/10 px-1.5 py-0.5 text-xs text-green-500">
238287
{testResult.tools.length} tools
239288
</span>
240289
)}
241290
</div>
242-
<p className="text-muted-foreground mt-0.5 text-xs break-all">{command}</p>
291+
{isEditing ? (
292+
<input
293+
type="text"
294+
value={editCommand}
295+
onChange={(e) => setEditCommand(e.target.value)}
296+
className="border-border-medium bg-secondary/30 text-foreground placeholder:text-muted-foreground focus:ring-accent mt-1 w-full rounded-md border px-2 py-1 text-xs focus:ring-1 focus:outline-none"
297+
autoFocus
298+
onKeyDown={(e) => {
299+
if (e.key === "Enter") {
300+
void handleSaveEdit();
301+
} else if (e.key === "Escape") {
302+
handleCancelEdit();
303+
}
304+
}}
305+
/>
306+
) : (
307+
<p className="text-muted-foreground mt-0.5 text-xs break-all">{command}</p>
308+
)}
243309
</div>
244310
<div className="flex shrink-0 gap-1">
245-
<button
246-
type="button"
247-
onClick={() => void handleTest(name)}
248-
disabled={isTesting}
249-
className="text-muted-foreground hover:bg-secondary hover:text-accent rounded p-1.5 transition-colors disabled:opacity-50"
250-
title="Test connection"
251-
>
252-
{isTesting ? (
253-
<Loader2 className="h-4 w-4 animate-spin" />
254-
) : (
255-
<Play className="h-4 w-4" />
256-
)}
257-
</button>
258-
<button
259-
type="button"
260-
onClick={() => void handleRemove(name)}
261-
disabled={loading}
262-
className="text-muted-foreground hover:bg-destructive/10 hover:text-destructive rounded p-1.5 transition-colors"
263-
title="Remove server"
264-
>
265-
<Trash2 className="h-4 w-4" />
266-
</button>
311+
{isEditing ? (
312+
<>
313+
<button
314+
type="button"
315+
onClick={() => void handleSaveEdit()}
316+
disabled={savingEdit || !editCommand.trim()}
317+
className="text-muted-foreground rounded p-1.5 transition-colors hover:bg-green-500/10 hover:text-green-500 disabled:opacity-50"
318+
title="Save (Enter)"
319+
>
320+
{savingEdit ? (
321+
<Loader2 className="h-4 w-4 animate-spin" />
322+
) : (
323+
<Check className="h-4 w-4" />
324+
)}
325+
</button>
326+
<button
327+
type="button"
328+
onClick={handleCancelEdit}
329+
disabled={savingEdit}
330+
className="text-muted-foreground hover:bg-destructive/10 hover:text-destructive rounded p-1.5 transition-colors"
331+
title="Cancel (Esc)"
332+
>
333+
<X className="h-4 w-4" />
334+
</button>
335+
</>
336+
) : (
337+
<>
338+
<button
339+
type="button"
340+
onClick={() => void handleTest(name)}
341+
disabled={isTesting}
342+
className="text-muted-foreground hover:bg-secondary hover:text-accent rounded p-1.5 transition-colors disabled:opacity-50"
343+
title="Test connection"
344+
>
345+
{isTesting ? (
346+
<Loader2 className="h-4 w-4 animate-spin" />
347+
) : (
348+
<Play className="h-4 w-4" />
349+
)}
350+
</button>
351+
<button
352+
type="button"
353+
onClick={() => handleStartEdit(name, command)}
354+
className="text-muted-foreground hover:bg-secondary hover:text-accent rounded p-1.5 transition-colors"
355+
title="Edit command"
356+
>
357+
<Pencil className="h-4 w-4" />
358+
</button>
359+
<button
360+
type="button"
361+
onClick={() => void handleRemove(name)}
362+
disabled={loading}
363+
className="text-muted-foreground hover:bg-destructive/10 hover:text-destructive rounded p-1.5 transition-colors"
364+
title="Remove server"
365+
>
366+
<Trash2 className="h-4 w-4" />
367+
</button>
368+
</>
369+
)}
267370
</div>
268371
</div>
269-
{testResult && !testResult.success && (
372+
{testResult && !testResult.success && !isEditing && (
270373
<div className="text-destructive mt-2 flex items-start gap-1.5 text-xs">
271374
<XCircle className="mt-0.5 h-3 w-3 shrink-0" />
272375
<span>{testResult.error}</span>
273376
</div>
274377
)}
275-
{testResult?.success && testResult.tools.length > 0 && (
378+
{testResult?.success && testResult.tools.length > 0 && !isEditing && (
276379
<p className="text-muted-foreground mt-2 text-xs">
277380
Tools: {testResult.tools.join(", ")}
278381
</p>

src/browser/utils/slashCommands/registry.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,18 @@ const mcpCommandDefinition: SlashCommandDefinition = {
606606
return { type: "mcp-add", name, command: commandText };
607607
}
608608

609+
if (sub === "edit") {
610+
const name = cleanRemainingTokens[1];
611+
const commandText = rawInput
612+
.trim()
613+
.replace(/^edit\s+[^\s]+\s*/i, "")
614+
.trim();
615+
if (!name || !commandText) {
616+
return { type: "unknown-command", command: "mcp", subcommand: "edit" };
617+
}
618+
return { type: "mcp-edit", name, command: commandText };
619+
}
620+
609621
if (sub === "remove") {
610622
const name = cleanRemainingTokens[1];
611623
if (!name) {

src/browser/utils/slashCommands/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export type ParsedCommand =
3030
}
3131
| { type: "vim-toggle" }
3232
| { type: "mcp-add"; name: string; command: string }
33+
| { type: "mcp-edit"; name: string; command: string }
3334
| { type: "mcp-remove"; name: string }
3435
| { type: "mcp-open" }
3536
| { type: "unknown-command"; command: string; subcommand?: string }

0 commit comments

Comments
 (0)