Skip to content

Commit 639ec0a

Browse files
committed
Add per-project MCP configuration support
- Extract MCP servers from ~/.claude.json for each project - Write project MCP config to .fleetcode/mcp-config-<hash>.json - Pass --mcp-config flag to Claude CLI on session start - Update MCP poller to run in project directory instead of worktree - Reduce first poll delay from 2s to 500ms for faster UI updates - Add unique UUID suffix to git branch names to prevent conflicts - Store mcpConfigPath in persisted sessions
1 parent 54e7a1a commit 639ec0a

File tree

1 file changed

+86
-18
lines changed

1 file changed

+86
-18
lines changed

main.ts

Lines changed: 86 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ interface PersistedSession {
2727
worktreePath: string;
2828
createdAt: number;
2929
sessionUuid: string;
30+
mcpConfigPath?: string;
3031
}
3132

3233
let mainWindow: BrowserWindow;
@@ -49,14 +50,61 @@ function getNextSessionNumber(): number {
4950
return Math.max(...sessions.map(s => s.number)) + 1;
5051
}
5152

53+
// Extract MCP config for a project from ~/.claude.json
54+
function extractProjectMcpConfig(projectDir: string): any {
55+
try {
56+
const claudeConfigPath = path.join(os.homedir(), ".claude.json");
57+
58+
if (!fs.existsSync(claudeConfigPath)) {
59+
return {};
60+
}
61+
62+
const claudeConfig = JSON.parse(fs.readFileSync(claudeConfigPath, "utf8"));
63+
64+
if (!claudeConfig.projects || !claudeConfig.projects[projectDir]) {
65+
return {};
66+
}
67+
68+
return claudeConfig.projects[projectDir].mcpServers || {};
69+
} catch (error) {
70+
console.error("Error extracting MCP config:", error);
71+
return {};
72+
}
73+
}
74+
75+
// Write MCP config file for a project (shared across all sessions)
76+
function writeMcpConfigFile(projectDir: string, mcpServers: any): string | null {
77+
try {
78+
// Create a hash of the project directory for unique filename
79+
const crypto = require("crypto");
80+
const hash = crypto.createHash("md5").update(projectDir).digest("hex").substring(0, 8);
81+
82+
const fleetcodeDir = path.join(projectDir, ".fleetcode");
83+
if (!fs.existsSync(fleetcodeDir)) {
84+
fs.mkdirSync(fleetcodeDir, { recursive: true });
85+
}
86+
87+
const configFilePath = path.join(fleetcodeDir, `mcp-config-${hash}.json`);
88+
const configContent = JSON.stringify({ mcpServers }, null, 2);
89+
90+
fs.writeFileSync(configFilePath, configContent, "utf8");
91+
92+
return configFilePath;
93+
} catch (error) {
94+
console.error("Error writing MCP config file:", error);
95+
return null;
96+
}
97+
}
98+
5299
// Spawn headless PTY for MCP polling
53-
function spawnMcpPoller(sessionId: string, worktreePath: string) {
100+
function spawnMcpPoller(sessionId: string, projectDir: string) {
101+
console.log(`[MCP Poller] Spawning for session ${sessionId}, project dir: ${projectDir}`);
54102
const shell = os.platform() === "darwin" ? "zsh" : "bash";
55103
const ptyProcess = pty.spawn(shell, ["-l"], {
56104
name: "xterm-color",
57105
cols: 80,
58106
rows: 30,
59-
cwd: worktreePath,
107+
cwd: projectDir,
60108
env: process.env,
61109
});
62110

@@ -68,49 +116,61 @@ function spawnMcpPoller(sessionId: string, worktreePath: string) {
68116
ptyProcess.onData((data) => {
69117
// Accumulate output without displaying it
70118
outputBuffer += data;
119+
console.log(`[MCP Poller ${sessionId}] Data:`, data.substring(0, 100));
71120

72121
// Parse output whenever we have MCP server entries
73122
// Match lines like: "servername: url (type) - ✓ Connected" or "servername: command (stdio) - ✓ Connected"
74123
// Pattern handles both SSE (with URLs) and stdio (with commands/paths)
75124
const mcpServerLineRegex = /^[\w-]+:.+\((?:SSE|stdio)\)\s+-\s+[]/m;
76125

77126
if (mcpServerLineRegex.test(data) || data.includes("No MCP servers configured")) {
127+
console.log(`[MCP Poller ${sessionId}] MCP output detected, parsing...`);
78128
try {
79129
const servers = parseMcpOutput(outputBuffer);
130+
console.log(`[MCP Poller ${sessionId}] Parsed servers:`, servers);
80131

81132
// Merge servers into the map (upsert by name)
82133
servers.forEach(server => {
83134
serverMap.set(server.name, server);
84135
});
85136

86137
const allServers = Array.from(serverMap.values());
138+
console.log(`[MCP Poller ${sessionId}] Total servers:`, allServers);
87139

88140
if (mainWindow && !mainWindow.isDestroyed()) {
89141
mainWindow.webContents.send("mcp-servers-updated", sessionId, allServers);
142+
console.log(`[MCP Poller ${sessionId}] Sent mcp-servers-updated event`);
90143
}
91144
} catch (error) {
92-
console.error("Error parsing MCP output:", error);
145+
console.error(`[MCP Poller ${sessionId}] Error parsing:`, error);
93146
}
94147
}
95148

96149
// Clear buffer when we see the shell prompt (command finished)
97150
if ((data.includes("% ") || data.includes("$ ") || data.includes("➜ ")) &&
98151
outputBuffer.includes("claude mcp list")) {
152+
console.log(`[MCP Poller ${sessionId}] Command complete, clearing buffer`);
99153
outputBuffer = "";
100154
}
101155
});
102156

103-
// Wait for shell to be ready, then start polling loop
157+
// Start polling immediately and then every 60 seconds
158+
console.log(`[MCP Poller ${sessionId}] Starting polling loop`);
159+
const pollMcp = () => {
160+
if (mcpPollerPtyProcesses.has(sessionId)) {
161+
const command = `claude mcp list`;
162+
console.log(`[MCP Poller ${sessionId}] Running command: ${command}`);
163+
ptyProcess.write(command + "\r");
164+
setTimeout(pollMcp, 60000);
165+
} else {
166+
console.log(`[MCP Poller ${sessionId}] No longer active, stopping`);
167+
}
168+
};
169+
170+
// Wait briefly for shell to be ready before first poll
104171
setTimeout(() => {
105-
// Run `claude mcp list` every 60 seconds
106-
const pollMcp = () => {
107-
if (mcpPollerPtyProcesses.has(sessionId)) {
108-
ptyProcess.write("claude mcp list\r");
109-
setTimeout(pollMcp, 60000);
110-
}
111-
};
112172
pollMcp();
113-
}, 2000); // Wait 2s for shell to initialize
173+
}, 500);
114174
}
115175

116176
// Parse MCP server list output
@@ -157,7 +217,9 @@ function spawnSessionPty(
157217
worktreePath: string,
158218
config: SessionConfig,
159219
sessionUuid: string,
160-
isNewSession: boolean
220+
isNewSession: boolean,
221+
mcpConfigPath?: string,
222+
projectDir?: string
161223
) {
162224
const shell = os.platform() === "darwin" ? "zsh" : "bash";
163225
const ptyProcess = pty.spawn(shell, ["-l"], {
@@ -211,7 +273,8 @@ function spawnSessionPty(
211273
? `--session-id ${sessionUuid}`
212274
: `--resume ${sessionUuid}`;
213275
const skipPermissionsFlag = config.skipPermissions ? "--dangerously-skip-permissions" : "";
214-
const flags = [sessionFlag, skipPermissionsFlag].filter(f => f).join(" ");
276+
const mcpConfigFlag = mcpConfigPath ? `--mcp-config ${mcpConfigPath}` : "";
277+
const flags = [sessionFlag, skipPermissionsFlag, mcpConfigFlag].filter(f => f).join(" ");
215278
const claudeCmd = `claude ${flags}\r`;
216279
ptyProcess.write(claudeCmd);
217280
} else if (config.codingAgent === "codex") {
@@ -226,8 +289,8 @@ function spawnSessionPty(
226289
claudeReady = true;
227290
// Spawn MCP poller now that Claude is authenticated and ready
228291
// Check if poller doesn't already exist to prevent duplicates
229-
if (!mcpPollerPtyProcesses.has(sessionId)) {
230-
spawnMcpPoller(sessionId, worktreePath);
292+
if (!mcpPollerPtyProcesses.has(sessionId) && projectDir) {
293+
spawnMcpPoller(sessionId, projectDir);
231294
}
232295
}
233296
}
@@ -377,6 +440,10 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
377440
// Create git worktree with unique branch name
378441
const worktreePath = await createWorktree(config.projectDir, config.parentBranch, sessionNumber, sessionUuid);
379442

443+
// Extract and write MCP config
444+
const mcpServers = extractProjectMcpConfig(config.projectDir);
445+
const mcpConfigPath = writeMcpConfigFile(config.projectDir, mcpServers);
446+
380447
// Create persisted session metadata
381448
const persistedSession: PersistedSession = {
382449
id: sessionId,
@@ -386,6 +453,7 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
386453
worktreePath,
387454
createdAt: Date.now(),
388455
sessionUuid,
456+
mcpConfigPath: mcpConfigPath || undefined,
389457
};
390458

391459
// Save to store
@@ -394,7 +462,7 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
394462
savePersistedSessions(sessions);
395463

396464
// Spawn PTY in worktree directory
397-
spawnSessionPty(sessionId, worktreePath, config, sessionUuid, true);
465+
spawnSessionPty(sessionId, worktreePath, config, sessionUuid, true, mcpConfigPath || undefined, config.projectDir);
398466

399467
event.reply("session-created", sessionId, persistedSession);
400468
} catch (error) {
@@ -438,7 +506,7 @@ ipcMain.on("reopen-session", (event, sessionId: string) => {
438506
}
439507

440508
// Spawn new PTY in worktree directory
441-
spawnSessionPty(sessionId, session.worktreePath, session.config, session.sessionUuid, false);
509+
spawnSessionPty(sessionId, session.worktreePath, session.config, session.sessionUuid, false, session.mcpConfigPath, session.config.projectDir);
442510

443511
event.reply("session-reopened", sessionId);
444512
});

0 commit comments

Comments
 (0)