add mcp server working

This commit is contained in:
Amrit Subramanian 2025-10-03 00:25:36 -04:00
parent 1d873aa82a
commit 1c614c9d28
5 changed files with 443 additions and 10 deletions

View file

@ -23,15 +23,20 @@
<div class="sidebar-content">
<div class="sidebar-section">
<div class="sidebar-section-title">SESSIONS</div>
<div class="sidebar-section-header">
<div class="sidebar-section-title">SESSIONS</div>
<button id="new-session" class="section-add-btn" title="New Session">+</button>
</div>
<div id="session-list"></div>
</div>
</div>
<div class="sidebar-footer">
<button id="new-session" class="btn-primary">
+ New Session
</button>
<div class="sidebar-section">
<div class="sidebar-section-header">
<div class="sidebar-section-title">MCP SERVERS</div>
<button id="add-mcp-server" class="section-add-btn" title="Add MCP Server">+</button>
</div>
<div id="mcp-server-list"></div>
</div>
</div>
</div>
@ -80,6 +85,67 @@
</div>
</div>
<!-- MCP Server Modal -->
<div id="mcp-modal" class="modal-overlay hidden">
<div class="modal">
<h2 class="modal-title">Add MCP Server</h2>
<div class="form-group">
<label class="form-label">Server Name</label>
<input type="text" id="mcp-name" class="form-input" placeholder="e.g., my-server" />
</div>
<div class="form-group">
<label class="form-label">Server Type</label>
<select id="mcp-type" class="form-select">
<option value="local">Local</option>
<option value="remote">Remote</option>
</select>
</div>
<!-- Local Server Fields -->
<div id="local-fields">
<div class="form-group">
<label class="form-label">Command</label>
<input type="text" id="mcp-command" class="form-input" placeholder="e.g., node, npx" />
</div>
<div class="form-group">
<label class="form-label">Arguments (space-separated)</label>
<input type="text" id="mcp-args" class="form-input" placeholder="e.g., /path/to/server.js or -s @modelcontextprotocol/server-filesystem" />
</div>
<div class="form-group">
<label class="form-label">Environment Variables (optional, JSON)</label>
<textarea id="mcp-env" class="form-input" rows="3" placeholder='{"API_KEY": "your_api_key"}'></textarea>
</div>
</div>
<!-- Remote Server Fields -->
<div id="remote-fields" style="display: none;">
<div class="form-group">
<label class="form-label">Server URL</label>
<input type="text" id="mcp-url" class="form-input" placeholder="https://your-server-url.com/mcp" />
</div>
<div class="form-group">
<label class="form-label">Headers (optional, JSON)</label>
<textarea id="mcp-headers" class="form-input" rows="3" placeholder='{"Authorization": "Bearer your-token"}'></textarea>
</div>
</div>
<div class="form-group">
<label class="form-label">Always Allow Tools (optional, comma-separated)</label>
<input type="text" id="mcp-always-allow" class="form-input" placeholder="e.g., tool1, tool2, tool3" />
</div>
<div class="btn-group">
<button id="cancel-mcp" class="btn-secondary">Cancel</button>
<button id="add-mcp" class="btn-primary">Add Server</button>
</div>
</div>
</div>
<script>
require('./dist/renderer.js');
</script>

80
main.ts
View file

@ -5,6 +5,10 @@ import * as path from "path";
import * as fs from "fs";
import { simpleGit } from "simple-git";
import Store from "electron-store";
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
interface SessionConfig {
projectDir: string;
@ -328,6 +332,82 @@ ipcMain.handle("get-all-sessions", () => {
return getPersistedSessions();
});
// MCP Server management functions
async function listMcpServers() {
try {
const { stdout } = await execAsync("claude mcp list");
if (stdout.includes("No MCP servers configured")) {
return [];
}
const lines = stdout.trim().split("\n").filter(line => line.trim());
const servers = [];
for (const line of lines) {
// Skip header lines, empty lines, and status messages
if (line.includes("MCP servers") ||
line.includes("---") ||
line.includes("Checking") ||
line.includes("health") ||
!line.trim()) {
continue;
}
// Parse format: "name: url (type) - status" or just "name"
// Extract just the server name (before the colon)
const colonIndex = line.indexOf(":");
const serverName = colonIndex > 0 ? line.substring(0, colonIndex).trim() : line.trim();
if (serverName) {
servers.push({ name: serverName });
}
}
return servers;
} catch (error) {
console.error("Error listing MCP servers:", error);
return [];
}
}
async function addMcpServer(name: string, config: any) {
// Use add-json to support full configuration including env vars, headers, etc.
const jsonConfig = JSON.stringify(config);
await execAsync(`claude mcp add-json "${name}" '${jsonConfig}'`);
}
async function removeMcpServer(name: string) {
await execAsync(`claude mcp remove "${name}"`);
}
ipcMain.handle("list-mcp-servers", async () => {
try {
return await listMcpServers();
} catch (error) {
console.error("Error listing MCP servers:", error);
return [];
}
});
ipcMain.handle("add-mcp-server", async (_event, name: string, config: any) => {
try {
await addMcpServer(name, config);
} catch (error) {
console.error("Error adding MCP server:", error);
throw error;
}
});
ipcMain.handle("remove-mcp-server", async (_event, name: string) => {
try {
await removeMcpServer(name);
} catch (error) {
console.error("Error removing MCP server:", error);
throw error;
}
});
const createWindow = () => {
mainWindow = new BrowserWindow({
width: 1400,

View file

@ -28,8 +28,18 @@ interface Session {
hasActivePty: boolean;
}
interface McpServer {
name: string;
command?: string;
args?: string[];
env?: Record<string, string>;
url?: string;
type?: "stdio" | "sse";
}
const sessions = new Map<string, Session>();
let activeSessionId: string | null = null;
let mcpServers: McpServer[] = [];
function createTerminalUI(sessionId: string) {
const term = new Terminal({
@ -461,3 +471,176 @@ createBtn?.addEventListener("click", () => {
parentBranchSelect.innerHTML = '<option value="">Loading branches...</option>';
codingAgentSelect.value = "claude";
});
// MCP Server management functions
async function loadMcpServers() {
try {
const servers = await ipcRenderer.invoke("list-mcp-servers");
mcpServers = servers;
renderMcpServers();
} catch (error) {
console.error("Failed to load MCP servers:", error);
}
}
function renderMcpServers() {
const list = document.getElementById("mcp-server-list");
if (!list) return;
list.innerHTML = "";
mcpServers.forEach(server => {
const item = document.createElement("div");
item.className = "session-list-item";
item.innerHTML = `
<div class="flex items-center space-x-2 flex-1">
<span class="session-indicator active"></span>
<span class="truncate">${server.name}</span>
</div>
<button class="session-delete-btn mcp-remove-btn" data-name="${server.name}" title="Remove server">×</button>
`;
const removeBtn = item.querySelector(".mcp-remove-btn");
removeBtn?.addEventListener("click", async (e) => {
e.stopPropagation();
if (confirm(`Remove MCP server "${server.name}"?`)) {
try {
await ipcRenderer.invoke("remove-mcp-server", server.name);
await loadMcpServers();
} catch (error) {
alert(`Failed to remove server: ${error}`);
}
}
});
list.appendChild(item);
});
}
// MCP Modal handling
const mcpModal = document.getElementById("mcp-modal");
const mcpNameInput = document.getElementById("mcp-name") as HTMLInputElement;
const mcpTypeSelect = document.getElementById("mcp-type") as HTMLSelectElement;
const mcpCommandInput = document.getElementById("mcp-command") as HTMLInputElement;
const mcpArgsInput = document.getElementById("mcp-args") as HTMLInputElement;
const mcpEnvInput = document.getElementById("mcp-env") as HTMLTextAreaElement;
const mcpUrlInput = document.getElementById("mcp-url") as HTMLInputElement;
const mcpHeadersInput = document.getElementById("mcp-headers") as HTMLTextAreaElement;
const mcpAlwaysAllowInput = document.getElementById("mcp-always-allow") as HTMLInputElement;
const localFields = document.getElementById("local-fields");
const remoteFields = document.getElementById("remote-fields");
const cancelMcpBtn = document.getElementById("cancel-mcp");
const addMcpBtn = document.getElementById("add-mcp");
// Toggle fields based on server type
mcpTypeSelect?.addEventListener("change", () => {
if (mcpTypeSelect.value === "local") {
localFields!.style.display = "block";
remoteFields!.style.display = "none";
} else {
localFields!.style.display = "none";
remoteFields!.style.display = "block";
}
});
// Add MCP server button - opens modal
document.getElementById("add-mcp-server")?.addEventListener("click", () => {
mcpModal?.classList.remove("hidden");
mcpNameInput.value = "";
mcpTypeSelect.value = "local";
mcpCommandInput.value = "";
mcpArgsInput.value = "";
mcpEnvInput.value = "";
mcpUrlInput.value = "";
mcpHeadersInput.value = "";
mcpAlwaysAllowInput.value = "";
localFields!.style.display = "block";
remoteFields!.style.display = "none";
});
// Cancel MCP button
cancelMcpBtn?.addEventListener("click", () => {
mcpModal?.classList.add("hidden");
});
// Add MCP button
addMcpBtn?.addEventListener("click", async () => {
const name = mcpNameInput.value.trim();
const serverType = mcpTypeSelect.value;
if (!name) {
alert("Please enter a server name");
return;
}
const config: any = {};
if (serverType === "local") {
config.type = "stdio";
const command = mcpCommandInput.value.trim();
const argsInput = mcpArgsInput.value.trim();
if (!command) {
alert("Please enter a command");
return;
}
config.command = command;
if (argsInput) {
config.args = argsInput.split(" ").filter(a => a.trim());
}
// Parse environment variables if provided
const envInput = mcpEnvInput.value.trim();
if (envInput) {
try {
config.env = JSON.parse(envInput);
} catch (error) {
alert("Invalid JSON for environment variables");
return;
}
}
} else {
// Remote server
config.type = "sse";
const url = mcpUrlInput.value.trim();
if (!url) {
alert("Please enter a server URL");
return;
}
config.url = url;
// Parse headers if provided
const headersInput = mcpHeadersInput.value.trim();
if (headersInput) {
try {
config.headers = JSON.parse(headersInput);
} catch (error) {
alert("Invalid JSON for headers");
return;
}
}
}
// Parse always allow tools
const alwaysAllowInput = mcpAlwaysAllowInput.value.trim();
if (alwaysAllowInput) {
config.alwaysAllow = alwaysAllowInput.split(",").map(t => t.trim()).filter(t => t);
}
try {
await ipcRenderer.invoke("add-mcp-server", name, config);
await loadMcpServers();
mcpModal?.classList.add("hidden");
} catch (error) {
console.error("Error adding server:", error);
alert(`Failed to add server: ${error}`);
}
});
// Load MCP servers on startup
loadMcpServers();

98
simple-claude.ts Normal file
View file

@ -0,0 +1,98 @@
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
export interface McpServer {
name: string;
command?: string;
args?: string[];
env?: Record<string, string>;
url?: string;
type?: "stdio" | "sse";
}
export class SimpleClaude {
private claudePath: string;
constructor(claudePath: string = "claude") {
this.claudePath = claudePath;
}
async listMcpServers(): Promise<McpServer[]> {
try {
const { stdout } = await execAsync(`${this.claudePath} mcp list`);
// If no servers configured, return empty array
if (stdout.includes("No MCP servers configured")) {
return [];
}
// Parse the output - format is typically a list of server names
// We'll need to get details for each server
const lines = stdout.trim().split("\n").filter(line => line.trim());
const servers: McpServer[] = [];
for (const line of lines) {
// Skip header lines
if (line.includes("MCP servers") || line.includes("---")) {
continue;
}
const serverName = line.trim();
if (serverName) {
try {
const details = await this.getMcpServer(serverName);
servers.push(details);
} catch (error) {
// If we can't get details, just add the name
servers.push({ name: serverName });
}
}
}
return servers;
} catch (error) {
console.error("Error listing MCP servers:", error);
return [];
}
}
async getMcpServer(name: string): Promise<McpServer> {
try {
const { stdout } = await execAsync(`${this.claudePath} mcp get "${name}"`);
// Try to parse as JSON if possible
try {
const parsed = JSON.parse(stdout);
return { name, ...parsed };
} catch {
// If not JSON, return basic info
return { name };
}
} catch (error) {
throw new Error(`Failed to get MCP server ${name}: ${error}`);
}
}
async addMcpServer(name: string, command: string, args: string[] = []): Promise<void> {
try {
const argsStr = args.join(" ");
await execAsync(`${this.claudePath} mcp add "${name}" ${command} ${argsStr}`);
} catch (error) {
throw new Error(`Failed to add MCP server ${name}: ${error}`);
}
}
async removeMcpServer(name: string): Promise<void> {
try {
await execAsync(`${this.claudePath} mcp remove "${name}"`);
} catch (error) {
throw new Error(`Failed to remove MCP server ${name}: ${error}`);
}
}
}
export function simpleClaude(claudePath?: string): SimpleClaude {
return new SimpleClaude(claudePath);
}

View file

@ -24,12 +24,16 @@
@apply p-2;
}
.sidebar-section-title {
@apply text-xs text-gray-400 px-2 py-1 mb-1;
.sidebar-section-header {
@apply flex items-center justify-between px-2 py-1 mb-1;
}
.sidebar-footer {
@apply p-3 border-t border-gray-700;
.sidebar-section-title {
@apply text-xs text-gray-400;
}
.section-add-btn {
@apply text-gray-400 hover:text-white hover:bg-gray-700 text-sm w-5 h-5 flex items-center justify-center rounded transition-all;
}
/* Session List Item */
@ -122,6 +126,8 @@
.form-input {
@apply w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500;
font-family: inherit;
resize: vertical;
}
.form-select {