add mcp server working
This commit is contained in:
parent
1d873aa82a
commit
1c614c9d28
78
index.html
78
index.html
|
|
@ -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
80
main.ts
|
|
@ -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,
|
||||
|
|
|
|||
183
renderer.ts
183
renderer.ts
|
|
@ -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
98
simple-claude.ts
Normal 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);
|
||||
}
|
||||
14
styles.css
14
styles.css
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue