Skip to content

Commit 1c614c9

Browse files
committed
add mcp server working
1 parent 1d873aa commit 1c614c9

File tree

5 files changed

+442
-9
lines changed

5 files changed

+442
-9
lines changed

index.html

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,20 @@ <h1 class="sidebar-title">FleetCode</h1>
2323

2424
<div class="sidebar-content">
2525
<div class="sidebar-section">
26-
<div class="sidebar-section-title">SESSIONS</div>
26+
<div class="sidebar-section-header">
27+
<div class="sidebar-section-title">SESSIONS</div>
28+
<button id="new-session" class="section-add-btn" title="New Session">+</button>
29+
</div>
2730
<div id="session-list"></div>
2831
</div>
29-
</div>
3032

31-
<div class="sidebar-footer">
32-
<button id="new-session" class="btn-primary">
33-
+ New Session
34-
</button>
33+
<div class="sidebar-section">
34+
<div class="sidebar-section-header">
35+
<div class="sidebar-section-title">MCP SERVERS</div>
36+
<button id="add-mcp-server" class="section-add-btn" title="Add MCP Server">+</button>
37+
</div>
38+
<div id="mcp-server-list"></div>
39+
</div>
3540
</div>
3641
</div>
3742

@@ -80,6 +85,67 @@ <h2 class="modal-title">New Session Configuration</h2>
8085
</div>
8186
</div>
8287

88+
<!-- MCP Server Modal -->
89+
<div id="mcp-modal" class="modal-overlay hidden">
90+
<div class="modal">
91+
<h2 class="modal-title">Add MCP Server</h2>
92+
93+
<div class="form-group">
94+
<label class="form-label">Server Name</label>
95+
<input type="text" id="mcp-name" class="form-input" placeholder="e.g., my-server" />
96+
</div>
97+
98+
<div class="form-group">
99+
<label class="form-label">Server Type</label>
100+
<select id="mcp-type" class="form-select">
101+
<option value="local">Local</option>
102+
<option value="remote">Remote</option>
103+
</select>
104+
</div>
105+
106+
<!-- Local Server Fields -->
107+
<div id="local-fields">
108+
<div class="form-group">
109+
<label class="form-label">Command</label>
110+
<input type="text" id="mcp-command" class="form-input" placeholder="e.g., node, npx" />
111+
</div>
112+
113+
<div class="form-group">
114+
<label class="form-label">Arguments (space-separated)</label>
115+
<input type="text" id="mcp-args" class="form-input" placeholder="e.g., /path/to/server.js or -s @modelcontextprotocol/server-filesystem" />
116+
</div>
117+
118+
<div class="form-group">
119+
<label class="form-label">Environment Variables (optional, JSON)</label>
120+
<textarea id="mcp-env" class="form-input" rows="3" placeholder='{"API_KEY": "your_api_key"}'></textarea>
121+
</div>
122+
</div>
123+
124+
<!-- Remote Server Fields -->
125+
<div id="remote-fields" style="display: none;">
126+
<div class="form-group">
127+
<label class="form-label">Server URL</label>
128+
<input type="text" id="mcp-url" class="form-input" placeholder="https://your-server-url.com/mcp" />
129+
</div>
130+
131+
<div class="form-group">
132+
<label class="form-label">Headers (optional, JSON)</label>
133+
<textarea id="mcp-headers" class="form-input" rows="3" placeholder='{"Authorization": "Bearer your-token"}'></textarea>
134+
</div>
135+
</div>
136+
137+
<div class="form-group">
138+
<label class="form-label">Always Allow Tools (optional, comma-separated)</label>
139+
<input type="text" id="mcp-always-allow" class="form-input" placeholder="e.g., tool1, tool2, tool3" />
140+
</div>
141+
142+
<div class="btn-group">
143+
<button id="cancel-mcp" class="btn-secondary">Cancel</button>
144+
<button id="add-mcp" class="btn-primary">Add Server</button>
145+
</div>
146+
</div>
147+
</div>
148+
83149
<script>
84150
require('./dist/renderer.js');
85151
</script>

main.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import * as path from "path";
55
import * as fs from "fs";
66
import { simpleGit } from "simple-git";
77
import Store from "electron-store";
8+
import { exec } from "child_process";
9+
import { promisify } from "util";
10+
11+
const execAsync = promisify(exec);
812

913
interface SessionConfig {
1014
projectDir: string;
@@ -328,6 +332,82 @@ ipcMain.handle("get-all-sessions", () => {
328332
return getPersistedSessions();
329333
});
330334

335+
// MCP Server management functions
336+
async function listMcpServers() {
337+
try {
338+
const { stdout } = await execAsync("claude mcp list");
339+
340+
if (stdout.includes("No MCP servers configured")) {
341+
return [];
342+
}
343+
344+
const lines = stdout.trim().split("\n").filter(line => line.trim());
345+
const servers = [];
346+
347+
for (const line of lines) {
348+
// Skip header lines, empty lines, and status messages
349+
if (line.includes("MCP servers") ||
350+
line.includes("---") ||
351+
line.includes("Checking") ||
352+
line.includes("health") ||
353+
!line.trim()) {
354+
continue;
355+
}
356+
357+
// Parse format: "name: url (type) - status" or just "name"
358+
// Extract just the server name (before the colon)
359+
const colonIndex = line.indexOf(":");
360+
const serverName = colonIndex > 0 ? line.substring(0, colonIndex).trim() : line.trim();
361+
362+
if (serverName) {
363+
servers.push({ name: serverName });
364+
}
365+
}
366+
367+
return servers;
368+
} catch (error) {
369+
console.error("Error listing MCP servers:", error);
370+
return [];
371+
}
372+
}
373+
374+
async function addMcpServer(name: string, config: any) {
375+
// Use add-json to support full configuration including env vars, headers, etc.
376+
const jsonConfig = JSON.stringify(config);
377+
await execAsync(`claude mcp add-json "${name}" '${jsonConfig}'`);
378+
}
379+
380+
async function removeMcpServer(name: string) {
381+
await execAsync(`claude mcp remove "${name}"`);
382+
}
383+
384+
ipcMain.handle("list-mcp-servers", async () => {
385+
try {
386+
return await listMcpServers();
387+
} catch (error) {
388+
console.error("Error listing MCP servers:", error);
389+
return [];
390+
}
391+
});
392+
393+
ipcMain.handle("add-mcp-server", async (_event, name: string, config: any) => {
394+
try {
395+
await addMcpServer(name, config);
396+
} catch (error) {
397+
console.error("Error adding MCP server:", error);
398+
throw error;
399+
}
400+
});
401+
402+
ipcMain.handle("remove-mcp-server", async (_event, name: string) => {
403+
try {
404+
await removeMcpServer(name);
405+
} catch (error) {
406+
console.error("Error removing MCP server:", error);
407+
throw error;
408+
}
409+
});
410+
331411
const createWindow = () => {
332412
mainWindow = new BrowserWindow({
333413
width: 1400,

renderer.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,18 @@ interface Session {
2828
hasActivePty: boolean;
2929
}
3030

31+
interface McpServer {
32+
name: string;
33+
command?: string;
34+
args?: string[];
35+
env?: Record<string, string>;
36+
url?: string;
37+
type?: "stdio" | "sse";
38+
}
39+
3140
const sessions = new Map<string, Session>();
3241
let activeSessionId: string | null = null;
42+
let mcpServers: McpServer[] = [];
3343

3444
function createTerminalUI(sessionId: string) {
3545
const term = new Terminal({
@@ -461,3 +471,176 @@ createBtn?.addEventListener("click", () => {
461471
parentBranchSelect.innerHTML = '<option value="">Loading branches...</option>';
462472
codingAgentSelect.value = "claude";
463473
});
474+
475+
// MCP Server management functions
476+
async function loadMcpServers() {
477+
try {
478+
const servers = await ipcRenderer.invoke("list-mcp-servers");
479+
mcpServers = servers;
480+
renderMcpServers();
481+
} catch (error) {
482+
console.error("Failed to load MCP servers:", error);
483+
}
484+
}
485+
486+
function renderMcpServers() {
487+
const list = document.getElementById("mcp-server-list");
488+
if (!list) return;
489+
490+
list.innerHTML = "";
491+
492+
mcpServers.forEach(server => {
493+
const item = document.createElement("div");
494+
item.className = "session-list-item";
495+
item.innerHTML = `
496+
<div class="flex items-center space-x-2 flex-1">
497+
<span class="session-indicator active"></span>
498+
<span class="truncate">${server.name}</span>
499+
</div>
500+
<button class="session-delete-btn mcp-remove-btn" data-name="${server.name}" title="Remove server">×</button>
501+
`;
502+
503+
const removeBtn = item.querySelector(".mcp-remove-btn");
504+
removeBtn?.addEventListener("click", async (e) => {
505+
e.stopPropagation();
506+
if (confirm(`Remove MCP server "${server.name}"?`)) {
507+
try {
508+
await ipcRenderer.invoke("remove-mcp-server", server.name);
509+
await loadMcpServers();
510+
} catch (error) {
511+
alert(`Failed to remove server: ${error}`);
512+
}
513+
}
514+
});
515+
516+
list.appendChild(item);
517+
});
518+
}
519+
520+
// MCP Modal handling
521+
const mcpModal = document.getElementById("mcp-modal");
522+
const mcpNameInput = document.getElementById("mcp-name") as HTMLInputElement;
523+
const mcpTypeSelect = document.getElementById("mcp-type") as HTMLSelectElement;
524+
const mcpCommandInput = document.getElementById("mcp-command") as HTMLInputElement;
525+
const mcpArgsInput = document.getElementById("mcp-args") as HTMLInputElement;
526+
const mcpEnvInput = document.getElementById("mcp-env") as HTMLTextAreaElement;
527+
const mcpUrlInput = document.getElementById("mcp-url") as HTMLInputElement;
528+
const mcpHeadersInput = document.getElementById("mcp-headers") as HTMLTextAreaElement;
529+
const mcpAlwaysAllowInput = document.getElementById("mcp-always-allow") as HTMLInputElement;
530+
const localFields = document.getElementById("local-fields");
531+
const remoteFields = document.getElementById("remote-fields");
532+
const cancelMcpBtn = document.getElementById("cancel-mcp");
533+
const addMcpBtn = document.getElementById("add-mcp");
534+
535+
// Toggle fields based on server type
536+
mcpTypeSelect?.addEventListener("change", () => {
537+
if (mcpTypeSelect.value === "local") {
538+
localFields!.style.display = "block";
539+
remoteFields!.style.display = "none";
540+
} else {
541+
localFields!.style.display = "none";
542+
remoteFields!.style.display = "block";
543+
}
544+
});
545+
546+
// Add MCP server button - opens modal
547+
document.getElementById("add-mcp-server")?.addEventListener("click", () => {
548+
mcpModal?.classList.remove("hidden");
549+
mcpNameInput.value = "";
550+
mcpTypeSelect.value = "local";
551+
mcpCommandInput.value = "";
552+
mcpArgsInput.value = "";
553+
mcpEnvInput.value = "";
554+
mcpUrlInput.value = "";
555+
mcpHeadersInput.value = "";
556+
mcpAlwaysAllowInput.value = "";
557+
localFields!.style.display = "block";
558+
remoteFields!.style.display = "none";
559+
});
560+
561+
// Cancel MCP button
562+
cancelMcpBtn?.addEventListener("click", () => {
563+
mcpModal?.classList.add("hidden");
564+
});
565+
566+
// Add MCP button
567+
addMcpBtn?.addEventListener("click", async () => {
568+
const name = mcpNameInput.value.trim();
569+
const serverType = mcpTypeSelect.value;
570+
571+
if (!name) {
572+
alert("Please enter a server name");
573+
return;
574+
}
575+
576+
const config: any = {};
577+
578+
if (serverType === "local") {
579+
config.type = "stdio";
580+
581+
const command = mcpCommandInput.value.trim();
582+
const argsInput = mcpArgsInput.value.trim();
583+
584+
if (!command) {
585+
alert("Please enter a command");
586+
return;
587+
}
588+
589+
config.command = command;
590+
if (argsInput) {
591+
config.args = argsInput.split(" ").filter(a => a.trim());
592+
}
593+
594+
// Parse environment variables if provided
595+
const envInput = mcpEnvInput.value.trim();
596+
if (envInput) {
597+
try {
598+
config.env = JSON.parse(envInput);
599+
} catch (error) {
600+
alert("Invalid JSON for environment variables");
601+
return;
602+
}
603+
}
604+
} else {
605+
// Remote server
606+
config.type = "sse";
607+
608+
const url = mcpUrlInput.value.trim();
609+
610+
if (!url) {
611+
alert("Please enter a server URL");
612+
return;
613+
}
614+
615+
config.url = url;
616+
617+
// Parse headers if provided
618+
const headersInput = mcpHeadersInput.value.trim();
619+
if (headersInput) {
620+
try {
621+
config.headers = JSON.parse(headersInput);
622+
} catch (error) {
623+
alert("Invalid JSON for headers");
624+
return;
625+
}
626+
}
627+
}
628+
629+
// Parse always allow tools
630+
const alwaysAllowInput = mcpAlwaysAllowInput.value.trim();
631+
if (alwaysAllowInput) {
632+
config.alwaysAllow = alwaysAllowInput.split(",").map(t => t.trim()).filter(t => t);
633+
}
634+
635+
try {
636+
await ipcRenderer.invoke("add-mcp-server", name, config);
637+
await loadMcpServers();
638+
mcpModal?.classList.add("hidden");
639+
} catch (error) {
640+
console.error("Error adding server:", error);
641+
alert(`Failed to add server: ${error}`);
642+
}
643+
});
644+
645+
// Load MCP servers on startup
646+
loadMcpServers();

0 commit comments

Comments
 (0)