diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 40f65e94..e3fa00f8 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -20,10 +20,12 @@ "dependencies": { "@zilliz/claude-context-core": "workspace:*", "@modelcontextprotocol/sdk": "^1.12.1", - "zod": "^3.25.55" + "zod": "^3.25.55", + "proper-lockfile": "^4.1.2" }, "devDependencies": { "@types/node": "^20.0.0", + "@types/proper-lockfile": "^4.1.4", "tsx": "^4.19.4", "typescript": "^5.0.0" }, diff --git a/packages/mcp/src/handlers.ts b/packages/mcp/src/handlers.ts index 1530d0c3..e61ff777 100644 --- a/packages/mcp/src/handlers.ts +++ b/packages/mcp/src/handlers.ts @@ -1,6 +1,7 @@ import * as fs from "fs"; import * as path from "path"; import * as crypto from "crypto"; +import { isCurrentProcessLeader, acquireLock } from './lock'; import { Context, COLLECTION_LIMIT_MESSAGE } from "@zilliz/claude-context-core"; import { SnapshotManager } from "./snapshot.js"; import { ensureAbsolutePath, truncateContent, trackCodebasePath } from "./utils.js"; @@ -29,6 +30,11 @@ export class ToolHandlers { * - If local snapshot is missing directories (exist in cloud), ignore them */ private async syncIndexedCodebasesFromCloud(): Promise { + if (!isCurrentProcessLeader()) { + console.log('[SYNC-CLOUD] This process is a follower. Skipping cloud sync.'); + return; + } + try { console.log(`[SYNC-CLOUD] 🔄 Syncing indexed codebases from Zilliz Cloud...`); @@ -142,6 +148,16 @@ export class ToolHandlers { } public async handleIndexCodebase(args: any) { + if (!isCurrentProcessLeader()) { + return { + content: [{ + type: "text", + text: "Another process is already indexing. This process is a follower and cannot index." + }], + isError: true + }; + } + const { path: codebasePath, force, splitter, customExtensions, ignorePatterns } = args; const forceReindex = force || false; const splitterType = splitter || 'ast'; // Default to AST @@ -569,6 +585,16 @@ export class ToolHandlers { } public async handleClearIndex(args: any) { + if (!isCurrentProcessLeader()) { + return { + content: [{ + type: "text", + text: "Another process is already indexing. This process is a follower and cannot index." + }], + isError: true + }; + } + const { path: codebasePath } = args; if (this.snapshotManager.getIndexedCodebases().length === 0 && this.snapshotManager.getIndexingCodebases().length === 0) { diff --git a/packages/mcp/src/lock.ts b/packages/mcp/src/lock.ts new file mode 100644 index 00000000..bcbc6a92 --- /dev/null +++ b/packages/mcp/src/lock.ts @@ -0,0 +1,77 @@ + +import os from 'os'; +import path from 'path'; +import fs from 'fs'; +import lockfile from 'proper-lockfile'; + +const CONTEXT_DIR = path.join(os.homedir(), '.context'); +const LOCK_FILE = path.join(CONTEXT_DIR, 'leader.lock'); + +// Ensure the .context directory exists +if (!fs.existsSync(CONTEXT_DIR)) { + fs.mkdirSync(CONTEXT_DIR, { recursive: true }); +} + +let isLeader = false; +let lockInterval: NodeJS.Timeout | undefined; + +export async function acquireLock(): Promise { + if (isLeader) { + return true; + } + try { + // Using flock is generally more reliable as the lock is released by the OS if the process dies. + // proper-lockfile will use flock on systems that support it (Linux, BSD, etc.) + // and fall back to other mechanisms on systems that don't (like Windows). + await lockfile.lock(LOCK_FILE, { retries: 0, realpath: false }); + isLeader = true; + console.log('Acquired leader lock. This process is now the leader.'); + if (lockInterval) { + clearInterval(lockInterval); + lockInterval = undefined; + } + return true; + } catch (error) { + console.log('Could not acquire leader lock, running as follower.'); + isLeader = false; + if (!lockInterval) { + lockInterval = setInterval(acquireLock, 5000); // Check every 5 seconds + } + return false; + } +} + +export async function releaseLock(): Promise { + if (isLeader) { + try { + await lockfile.unlock(LOCK_FILE, { realpath: false }); + isLeader = false; + console.log('Released leader lock.'); + } catch (error) { + console.error('Error releasing leader lock:', error); + } + } +} + +export function isCurrentProcessLeader(): boolean { + return isLeader; +} + +export function getLockFilePath(): string { + return LOCK_FILE; +} + +// Graceful shutdown +process.on('exit', async () => { + await releaseLock(); +}); + +process.on('SIGINT', async () => { + await releaseLock(); + process.exit(); +}); + +process.on('SIGTERM', async () => { + await releaseLock(); + process.exit(); +});