Claude Context MCP 服务器通过启动时初始同步、默认5分钟一次的周期轮询,以及对~/.context/.sync-trigger触发器文件的监视来实现后台索引的自动更新。为防止多个 MCP 实例同时同步,系统采用基于文件系统目录的全局锁机制,并通过 owner.json 中的随机 token 进行精确的锁所有权校验与过期清理。
Claude Context MCP 后台同步机制:触发器文件、跨进程锁与周期轮询
Claude Context 的语义代码搜索能力依赖于一个持续更新的向量索引。为了在后台自动保持索引与本地代码库的变更同步,MCP 服务器 (@zilliz/claude-context-mcp) 设计了一套精密的同步机制。这套机制融合了定时轮询、文件触发与进程协调,确保了索引的时效性与操作的原子性。
一、三种同步触发方式
服务器的 SyncManager 类负责管理所有同步任务。它通过以下三种方式启动同步:
1. 启动时初始同步 (Initial Sync)
当 MCP 服务器进程启动时,startBackgroundSync() 方法被调用。在设置了触发器文件监视器之后,它会安排一次初始同步。
// 源码:packages/mcp/src/sync.ts
// startBackgroundSync() 方法内
console.log(`[SYNC-DEBUG] Scheduling initial sync in ${DEFAULT_INITIAL_SYNC_DELAY_MS}ms...`);
setTimeout(async () => {
console.log('[SYNC-DEBUG] Executing initial sync after server startup');
try {
await this.handleSyncIndex();
} catch (error) {
// ... 错误处理
}
}, DEFAULT_INITIAL_SYNC_DELAY_MS);
初始同步会延迟 5 秒 (DEFAULT_INITIAL_SYNC_DELAY_MS = 5_000) 执行,目的是为了让服务器和环境变量加载等初始化工作完成。这次同步会遍历快照中记录的所有已索引代码库,检查并应用自上次索引以来的变更。
2. 周期性后台同步 (Background Sync)
初始同步之后,服务器会设置一个定时器,以固定的间隔反复执行同步检查。
// 源码:packages/mcp/src/sync.ts
const syncIntervalMs = getBackgroundSyncIntervalMs();
// ...
const syncInterval = setInterval(() => {
console.log('[SYNC-DEBUG] Executing scheduled periodic sync');
this.handleSyncIndex();
}, syncIntervalMs);
周期间隔默认为 5 分钟 (DEFAULT_SYNC_INTERVAL_MS = 5 * 60 * 1000)。这个间隔可以通过环境变量 CLAUDE_CONTEXT_SYNC_INTERVAL_MS 进行配置,但最小值被限制为 1 秒 (MIN_SYNC_INTERVAL_MS)。同时,可以通过设置环境变量 CLAUDE_CONTEXT_BACKGROUND_SYNC 为 false 来完全禁用周期性同步。
3. 触发器文件即时同步 (Trigger File Sync)
这是实现“即时”同步的关键。服务器会监视用户主目录下的一个特定触发器文件:~/.context/.sync-trigger。
// 源码:packages/mcp/src/sync.ts
private setupTriggerWatcher(): void {
// ...
const watcher = fs.watch(contextDir, { encoding: 'utf8' }, (_event, filename) => {
if (typeof filename !== 'string' || filename !== triggerFile) return;
// 使用防抖,避免短时间内多次触发
if (this.triggerDebounceTimer) clearTimeout(this.triggerDebounceTimer);
this.triggerDebounceTimer = setTimeout(() => {
console.log('[SYNC] 🔔 Trigger file detected, starting instant re-index...');
void this.handleSyncIndex().catch((error) => { /* ... */ });
}, 2000);
});
// ...
}
工作流程:当其他进程(例如集成在 Claude Code 中的 PostToolUse 钩子)在每次文件写入或编辑操作后,执行 touch ~/.context/.sync-trigger 时,文件监视器会检测到这个变更。它会有一个 2 秒的防抖延迟,然后触发一次完整的 handleSyncIndex()。这使得索引能够近乎实时地反映代码变更。
环境变量控制:这个监视器功能可以通过 CLAUDE_CONTEXT_TRIGGER_WATCHER 环境变量进行开关控制。设置为 false 或 0 可以禁用它,这在一些需要避免文件系统监视的沙盒环境或只读文件系统中可能有用。源码中的 isTriggerWatcherEnabled() 方法负责解析这个配置。
二、跨进程全局锁机制
当多个 Claude Code 会话或不同的 AI 助手客户端同时运行,并连接到同一个 MCP 服务器实例时,可能会同时触发多次同步。为避免资源浪费和数据竞争,系统使用了一个基于文件系统的全局锁。
锁的实现
锁的路径被固定为 ~/.context/mcp-sync.lock。获取锁的核心在于尝试创建这个目录。
// 源码:packages/mcp/src/sync.ts
private acquireGlobalSyncLock(): boolean {
const lockPath = this.getSyncLockPath();
const token = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
try {
fs.mkdirSync(lockPath);
// 锁获取成功,写入所有者信息
fs.writeFileSync(path.join(lockPath, "owner.json"), JSON.stringify({
pid: process.pid,
token,
acquiredAt: new Date().toISOString()
}, null, 2));
this.syncLockToken = token;
return true;
} catch (error: any) {
if (error?.code !== "EEXIST") {
// 其他错误,获取失败
return false;
}
// 目录已存在 (EEXIST),说明锁已被占用
// 进入 stale 锁检查逻辑...
}
}
EEXIST 处理与 Stale 锁清理
当 fs.mkdirSync 抛出 EEXIST 错误时,表明锁目录已存在。此时不会立即放弃,而是检查这个锁是否“过期”(stale)。
// 源码:packages/mcp/src/sync.ts (EEXIST 分支内部)
try {
const stat = fs.statSync(lockPath);
if (Date.now() - stat.mtimeMs > staleMs) {
console.warn(`[SYNC-DEBUG] Reclaiming stale global sync lock: ${lockPath}`);
// 将旧锁目录重命名并删除
fs.renameSync(lockPath, `${lockPath}.stale-${process.pid}-${Date.now()}`);
fs.rmSync(`${lockPath}.stale-...`, { recursive: true, force: true });
// 重新创建锁目录并获取
fs.mkdirSync(lockPath);
// ... 写入 owner.json
return true;
}
} catch (statError: any) {
// ... 处理错误
}
// 如果锁未过期,则跳过本次同步
return false;
stale 锁的超时时间默认为 10 分钟 (DEFAULT_SYNC_LOCK_STALE_MS),可以通过环境变量 CLAUDE_CONTEXT_SYNC_LOCK_STALE_MS 进行调整。这防止了因进程崩溃未能释放锁而导致同步永久阻塞。
锁的释放与校验
释放锁时,系统会通过比对 owner.json 中的 token 来验证所有权,防止误删其他进程的锁。
// 源码:packages/mcp/src/sync.ts
private releaseGlobalSyncLock(): void {
const lockPath = this.getSyncLockPath();
try {
const ownerPath = path.join(lockPath, "owner.json");
if (this.syncLockToken && fs.existsSync(ownerPath)) {
const owner = JSON.parse(fs.readFileSync(ownerPath, "utf8"));
// Token 不匹配,说明这不是本进程获取的锁,跳过释放
if (owner.token && owner.token !== this.syncLockToken) {
return;
}
}
fs.rmSync(lockPath, { recursive: true, force: true });
this.syncLockToken = null;
} catch (error: any) {
// ...
}
}
端到端测试脚本 (sync-lock-e2e.mjs) 通过模拟多个 worker 进程,验证了只有第一个获取到锁的进程会执行 reindexByChange,而其他进程会等待;同时也验证了 stale 锁能被正确清理并重用。
三、reindexByChange 增量同步流程
无论是由哪种触发器启动,最终的执行入口都是 handleSyncIndex() 方法。该方法的核心是为每一个已索引的代码库调用 context.reindexByChange()。
handleSyncIndex() 流程:
- 从
SnapshotManager获取所有已索引代码库的路径列表。 - 检查当前是否已有同步任务在运行 (
this.isSyncing)。 - 尝试获取全局同步锁。
- 成功获取锁后,遍历代码库列表,对每个路径调用
this.context.reindexByChange(codebasePath, ...)。 reindexByChange的内部逻辑(实现于@zilliz/claude-context-core)通常包括:- 加载快照:读取上次同步时保存的文件 Merkle 树快照。
- 检查变更 (
checkForChanges):扫描当前代码库,与快照对比,识别出新增、修改和删除的文件。 - 删除已移除文件的 Chunks:从向量数据库中移除对应于已删除文件的嵌入向量。
- 重新索引变更文件:仅对新增和修改的文件重新进行文本分割、嵌入生成和向量入库。
- 无论成功或失败,最终都会在
finally块中释放全局同步锁。
这个基于快照的增量索引机制是 Claude Context 增量同步机制 的具体实现,确保了同步过程的高效性。
Claude Code PostToolUse 钩子集成示例
为了利用触发器文件实现即时同步,你可以在 Claude Code 的配置中设置一个 PostToolUse 钩子。当 AI 助手使用 Write 或 Edit 工具修改文件后,钩子脚本会 touch 触发器文件。
一个简单的 .mcp.json 钩子配置片段可能如下所示:
{
"mcpServers": {
"claude-context": {
"command": "npx",
"args": ["-y", "@zilliz/claude-context-mcp"],
"env": {}
}
},
"postToolUse": [
{
"matcher": "^(Write|Edit)$",
"command": "touch ~/.context/.sync-trigger"
}
]
}
这样,每当代码被修改,Claude Context 的索引都会在几秒钟内开始更新。
FAQ
Q: 如何强制立即触发一次索引同步?
A: 你可以手动创建或更新触发器文件:touch ~/.context/.sync-trigger。MCP 服务器会监视这个文件,变更后将立即启动一次完整的索引同步检查。
Q: 全局锁的超时时间 (stale) 如何配置?
A: 通过设置环境变量 CLAUDE_CONTEXT_SYNC_LOCK_STALE_MS 来调整,单位是毫秒。例如,设置为 300000 (5分钟)。默认值为 600000 (10分钟)。
Q: 如果同步失败,会有什么表现?
A: 同步失败(例如网络问题或向量数据库错误)不会导致 MCP 服务器崩溃。错误会被捕获并记录到控制台日志中(以 [SYNC-DEBUG] 或 [BACKGROUND-INDEX] 开头)。索引状态会根据失败情况被标记为 indexfailed,下次同步周期会再次尝试。你可以参考 Claude Context 故障排除指南 查看日志和状态。