Skip to content

Commit 7e4c463

Browse files
committed
Fix Kuzu lock: connection cache per dbPath for MCP tools
- graph-store: getCachedOrOpenGraphStore(), invalidateGraphStoreCache() - MCP tools (context, query, impact, cypher, rename, indexDbGraph) use cache - indexDbGraph: invalidate before (re)index so write uses fresh connection - clean_index: invalidate affected paths before cleanIndex() Avoids 'Could not set lock on file' when tools open same DB repeatedly. Made-with: Cursor
1 parent efc7333 commit 7e4c463

9 files changed

Lines changed: 50 additions & 13 deletions

File tree

src/graph/graph-store.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/**
22
* GraphStore: open/create Kuzu DB, addDocument, addSymbol, addEdge, getConnection.
33
* Phase 2 step 7.
4+
* Connection cache: one store per dbPath per process to avoid Kuzu "Could not set lock on file".
45
*/
56

67
import * as kuzu from 'kuzu';
@@ -9,6 +10,8 @@ import { dirname } from 'path';
910
import type { NodeLabel, SymbolProps } from '../types.js';
1011
import { getSchemaDdl, NODE_TABLE } from './schema.js';
1112

13+
const storeCache = new Map<string, GraphStore>();
14+
1215
function escapeCypherString(s: string): string {
1316
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
1417
}
@@ -71,3 +74,23 @@ export async function openGraphStore(dbPath: string): Promise<GraphStore> {
7174
getConnection: () => conn,
7275
};
7376
}
77+
78+
/**
79+
* Return cached GraphStore for dbPath or open once and cache it.
80+
* Use this in MCP tools so repeated/concurrent requests share one connection per DB (avoids Kuzu lock errors).
81+
*/
82+
export async function getCachedOrOpenGraphStore(dbPath: string): Promise<GraphStore> {
83+
const cached = storeCache.get(dbPath);
84+
if (cached) return cached;
85+
const store = await openGraphStore(dbPath);
86+
storeCache.set(dbPath, store);
87+
return store;
88+
}
89+
90+
/**
91+
* Remove cached store for dbPath. Next getCachedOrOpenGraphStore(dbPath) will open a new connection.
92+
* Call before re-index or clean so the next access sees the new or removed DB.
93+
*/
94+
export function invalidateGraphStoreCache(dbPath: string): void {
95+
storeCache.delete(dbPath);
96+
}

src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ export * from './types.js';
66
export { findSysmlFiles } from './discovery/find-sysml.js';
77
export { applyLoadOrder } from './discovery/load-order.js';
88
export { runIndexer } from './indexer/indexer.js';
9-
export { openGraphStore } from './graph/graph-store.js';
9+
export {
10+
openGraphStore,
11+
getCachedOrOpenGraphStore,
12+
invalidateGraphStoreCache,
13+
} from './graph/graph-store.js';
1014
export { getStorageRoot, setStorageRoot, getDbPathForIndexedPath } from './storage/location.js';
1115
export { listIndexedPaths } from './storage/list.js';
1216
export { cleanIndex } from './storage/clean.js';

src/mcp/tools/clean-index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@
22
* MCP tool: clean_index – remove index for path or all.
33
*/
44

5+
import { getDbPathForIndexedPath } from '../../storage/location.js';
6+
import { listIndexedPaths } from '../../storage/list.js';
57
import { cleanIndex } from '../../storage/clean.js';
8+
import { invalidateGraphStoreCache } from '../../graph/graph-store.js';
69

710
export interface CleanIndexArgs {
811
path?: string;
912
}
1013

1114
export async function handleCleanIndex(args: CleanIndexArgs): Promise<{ ok: boolean; removed?: string[]; error?: string }> {
15+
if (args.path !== undefined && args.path !== '') {
16+
invalidateGraphStoreCache(getDbPathForIndexedPath(args.path));
17+
} else {
18+
const paths = await listIndexedPaths();
19+
for (const p of paths) invalidateGraphStoreCache(getDbPathForIndexedPath(p));
20+
}
1221
const result = await cleanIndex(args.path);
1322
if (!result.ok) return { ok: false, error: result.error };
1423
return { ok: true, removed: result.removed };

src/mcp/tools/context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { listIndexedPaths } from '../../storage/list.js';
77
import { getDbPathForIndexedPath } from '../../storage/location.js';
8-
import { openGraphStore } from '../../graph/graph-store.js';
8+
import { getCachedOrOpenGraphStore } from '../../graph/graph-store.js';
99
import { NODE_TABLE } from '../../graph/schema.js';
1010
import { EDGE_TYPES } from '../../graph/schema.js';
1111

@@ -19,7 +19,7 @@ export async function handleContext(args: ContextArgs): Promise<{ ok: boolean; n
1919
return { ok: false, error: 'No indexed paths; run indexDbGraph first' };
2020
}
2121
const dbPath = getDbPathForIndexedPath(paths[0]);
22-
const store = await openGraphStore(dbPath);
22+
const store = await getCachedOrOpenGraphStore(dbPath);
2323
const conn = store.getConnection();
2424
const name = (args.name || '').trim().replace(/\\/g, '\\\\').replace(/'/g, "\\'");
2525
if (!name) {

src/mcp/tools/cypher.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { listIndexedPaths } from '../../storage/list.js';
77
import { getDbPathForIndexedPath } from '../../storage/location.js';
8-
import { openGraphStore } from '../../graph/graph-store.js';
8+
import { getCachedOrOpenGraphStore } from '../../graph/graph-store.js';
99

1010
export interface CypherArgs {
1111
query: string;
@@ -17,7 +17,7 @@ export async function handleCypher(args: CypherArgs): Promise<{ ok: boolean; row
1717
return { ok: false, error: 'No indexed paths; run indexDbGraph first' };
1818
}
1919
const dbPath = getDbPathForIndexedPath(paths[0]);
20-
const store = await openGraphStore(dbPath);
20+
const store = await getCachedOrOpenGraphStore(dbPath);
2121
const conn = store.getConnection();
2222
try {
2323
const result = await conn.query(args.query.trim());

src/mcp/tools/impact.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { listIndexedPaths } from '../../storage/list.js';
77
import { getDbPathForIndexedPath } from '../../storage/location.js';
8-
import { openGraphStore } from '../../graph/graph-store.js';
8+
import { getCachedOrOpenGraphStore } from '../../graph/graph-store.js';
99
import { NODE_TABLE } from '../../graph/schema.js';
1010
import { EDGE_TYPES } from '../../graph/schema.js';
1111

@@ -20,7 +20,7 @@ export async function handleImpact(args: ImpactArgs): Promise<{ ok: boolean; nod
2020
return { ok: false, error: 'No indexed paths; run indexDbGraph first' };
2121
}
2222
const dbPath = getDbPathForIndexedPath(paths[0]);
23-
const store = await openGraphStore(dbPath);
23+
const store = await getCachedOrOpenGraphStore(dbPath);
2424
const conn = store.getConnection();
2525
const target = (args.target || '').trim().replace(/\\/g, '\\\\').replace(/'/g, "\\'");
2626
if (!target) {

src/mcp/tools/index-db-graph.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import { getDbPathForIndexedPath } from '../../storage/location.js';
66
import { addToRegistry } from '../../storage/registry.js';
7-
import { openGraphStore } from '../../graph/graph-store.js';
7+
import { getCachedOrOpenGraphStore, invalidateGraphStoreCache } from '../../graph/graph-store.js';
88
import { runIndexer } from '../../indexer/indexer.js';
99

1010
export interface IndexDbGraphArgs {
@@ -19,7 +19,8 @@ export async function handleIndexDbGraph(args: IndexDbGraphArgs): Promise<{ ok:
1919
}
2020
for (const p of paths) {
2121
const dbPath = getDbPathForIndexedPath(p);
22-
const store = await openGraphStore(dbPath);
22+
invalidateGraphStoreCache(dbPath);
23+
const store = await getCachedOrOpenGraphStore(dbPath);
2324
const result = await runIndexer(store, { roots: [p] });
2425
if (!result.ok) return { ok: false, error: result.error };
2526
await addToRegistry(p);

src/mcp/tools/query.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { listIndexedPaths } from '../../storage/list.js';
77
import { getDbPathForIndexedPath } from '../../storage/location.js';
8-
import { openGraphStore } from '../../graph/graph-store.js';
8+
import { getCachedOrOpenGraphStore } from '../../graph/graph-store.js';
99
import { NODE_TABLE } from '../../graph/schema.js';
1010

1111
export interface QueryArgs {
@@ -19,7 +19,7 @@ export async function handleQuery(args: QueryArgs): Promise<{ ok: boolean; nodes
1919
return { ok: false, error: 'No indexed paths; run indexDbGraph first' };
2020
}
2121
const dbPath = getDbPathForIndexedPath(paths[0]);
22-
const store = await openGraphStore(dbPath);
22+
const store = await getCachedOrOpenGraphStore(dbPath);
2323
const conn = store.getConnection();
2424
const q = (args.query || '').trim().toLowerCase();
2525
if (!q) {

src/mcp/tools/rename.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { listIndexedPaths } from '../../storage/list.js';
77
import { getDbPathForIndexedPath } from '../../storage/location.js';
8-
import { openGraphStore } from '../../graph/graph-store.js';
8+
import { getCachedOrOpenGraphStore } from '../../graph/graph-store.js';
99
import { NODE_TABLE } from '../../graph/schema.js';
1010

1111
export interface RenameArgs {
@@ -20,7 +20,7 @@ export async function handleRename(args: RenameArgs): Promise<{ ok: boolean; pre
2020
return { ok: false, error: 'No indexed paths; run indexDbGraph first' };
2121
}
2222
const dbPath = getDbPathForIndexedPath(paths[0]);
23-
const store = await openGraphStore(dbPath);
23+
const store = await getCachedOrOpenGraphStore(dbPath);
2424
const conn = store.getConnection();
2525
const symbol = (args.symbol || '').trim().replace(/\\/g, '\\\\').replace(/'/g, "\\'");
2626
const newName = (args.newName ?? '').trim();

0 commit comments

Comments
 (0)