@grafema/mcp 0.1.0-alpha.5 → 0.1.1-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -131,7 +131,11 @@ async function run() {
131
131
  console.log(`[Worker] Connecting to RFDB server: socket=${socketPath}, db=${dbPath}`);
132
132
  db = new RFDBServerBackend({ socketPath, dbPath });
133
133
  await db.connect();
134
- await db.clear();
134
+ // NOTE: db.clear() is NOT called here.
135
+ // MCP server clears DB INSIDE the analysis lock BEFORE spawning this worker.
136
+ // This prevents race conditions where concurrent analysis calls could both
137
+ // clear the database. Worker assumes DB is already clean.
138
+ // See: REG-159 implementation, Phase 2.5 (Worker Clear Coordination)
135
139
  sendProgress({ phase: 'discovery', message: 'Starting analysis...' });
136
140
  // Create orchestrator
137
141
  const orchestrator = new Orchestrator({
@@ -3,9 +3,19 @@
3
3
  */
4
4
  import type { GraphBackend } from '@grafema/types';
5
5
  /**
6
- * Ensure project is analyzed, optionally filtering to a single service
6
+ * Ensure project is analyzed, optionally filtering to a single service.
7
+ *
8
+ * CONCURRENCY: This function is protected by a global mutex.
9
+ * - Only one analysis can run at a time
10
+ * - Concurrent calls wait for the current analysis to complete
11
+ * - force=true while analysis is running returns an error immediately
12
+ *
13
+ * @param serviceName - Optional service to analyze (null = all)
14
+ * @param force - If true, clear DB and re-analyze even if already analyzed.
15
+ * ERROR if another analysis is already running.
16
+ * @throws Error if force=true and analysis is already running
7
17
  */
8
- export declare function ensureAnalyzed(serviceName?: string | null): Promise<GraphBackend>;
18
+ export declare function ensureAnalyzed(serviceName?: string | null, force?: boolean): Promise<GraphBackend>;
9
19
  /**
10
20
  * Discover services without running full analysis
11
21
  */
@@ -1 +1 @@
1
- {"version":3,"file":"analysis.d.ts","sourceRoot":"","sources":["../src/analysis.ts"],"names":[],"mappings":"AAAA;;GAEG;AAaH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD;;GAEG;AACH,wBAAsB,cAAc,CAAC,WAAW,GAAE,MAAM,GAAG,IAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CA2F7F;AAED;;GAEG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAyC3D"}
1
+ {"version":3,"file":"analysis.d.ts","sourceRoot":"","sources":["../src/analysis.ts"],"names":[],"mappings":"AAAA;;GAEG;AAeH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD;;;;;;;;;;;;GAYG;AACH,wBAAsB,cAAc,CAClC,WAAW,GAAE,MAAM,GAAG,IAAW,EACjC,KAAK,GAAE,OAAe,GACrB,OAAO,CAAC,YAAY,CAAC,CAwGvB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAyC3D"}
package/dist/analysis.js CHANGED
@@ -2,42 +2,57 @@
2
2
  * MCP Analysis Orchestration
3
3
  */
4
4
  import { Orchestrator } from '@grafema/core';
5
- import { getOrCreateBackend, getProjectPath, getIsAnalyzed, setIsAnalyzed, getAnalysisStatus, setAnalysisStatus, } from './state.js';
6
- import { loadConfig, loadCustomPlugins, BUILTIN_PLUGINS } from './config.js';
5
+ import { getOrCreateBackend, getProjectPath, getIsAnalyzed, setIsAnalyzed, getAnalysisStatus, setAnalysisStatus, isAnalysisRunning, acquireAnalysisLock, } from './state.js';
6
+ import { loadConfig, loadCustomPlugins, createPlugins } from './config.js';
7
7
  import { log } from './utils.js';
8
8
  /**
9
- * Ensure project is analyzed, optionally filtering to a single service
9
+ * Ensure project is analyzed, optionally filtering to a single service.
10
+ *
11
+ * CONCURRENCY: This function is protected by a global mutex.
12
+ * - Only one analysis can run at a time
13
+ * - Concurrent calls wait for the current analysis to complete
14
+ * - force=true while analysis is running returns an error immediately
15
+ *
16
+ * @param serviceName - Optional service to analyze (null = all)
17
+ * @param force - If true, clear DB and re-analyze even if already analyzed.
18
+ * ERROR if another analysis is already running.
19
+ * @throws Error if force=true and analysis is already running
10
20
  */
11
- export async function ensureAnalyzed(serviceName = null) {
21
+ export async function ensureAnalyzed(serviceName = null, force = false) {
12
22
  const db = await getOrCreateBackend();
13
23
  const projectPath = getProjectPath();
14
- const isAnalyzed = getIsAnalyzed();
15
- if (!isAnalyzed || serviceName) {
24
+ // CONCURRENCY CHECK: If force=true and analysis is running, error immediately
25
+ // This check is BEFORE acquiring lock to fail fast
26
+ if (force && isAnalysisRunning()) {
27
+ throw new Error('Analysis is already in progress. Cannot force re-analysis while another analysis is running. ' +
28
+ 'Wait for the current analysis to complete or check status with get_analysis_status.');
29
+ }
30
+ // Skip if already analyzed (and not forcing, and no service filter)
31
+ if (getIsAnalyzed() && !serviceName && !force) {
32
+ return db;
33
+ }
34
+ // Acquire lock (waits if another analysis is running)
35
+ const releaseLock = await acquireAnalysisLock();
36
+ try {
37
+ // Double-check after acquiring lock (another call might have completed analysis while we waited)
38
+ if (getIsAnalyzed() && !serviceName && !force) {
39
+ return db;
40
+ }
41
+ // Clear DB inside lock, BEFORE running analysis
42
+ // This is critical for worker coordination: MCP server clears DB here,
43
+ // worker does NOT call db.clear() (see analysis-worker.ts)
44
+ if (force || !getIsAnalyzed()) {
45
+ log('[Grafema MCP] Clearing database before analysis...');
46
+ if (db.clear) {
47
+ await db.clear();
48
+ }
49
+ setIsAnalyzed(false);
50
+ }
16
51
  log(`[Grafema MCP] Analyzing project: ${projectPath}${serviceName ? ` (service: ${serviceName})` : ''}`);
17
52
  const config = loadConfig(projectPath);
18
53
  const { pluginMap: customPluginMap } = await loadCustomPlugins(projectPath);
19
- // Merge builtin and custom plugins
20
- const availablePlugins = {
21
- ...BUILTIN_PLUGINS,
22
- ...Object.fromEntries(Object.entries(customPluginMap).map(([name, PluginClass]) => [
23
- name,
24
- () => new PluginClass(),
25
- ])),
26
- };
27
- // Build plugin list from config
28
- const plugins = [];
29
- for (const [phase, pluginNames] of Object.entries(config.plugins || {})) {
30
- for (const name of pluginNames) {
31
- const factory = availablePlugins[name];
32
- if (factory) {
33
- plugins.push(factory());
34
- log(`[Grafema MCP] Enabled plugin: ${name} (${phase})`);
35
- }
36
- else {
37
- log(`[Grafema MCP] Warning: Unknown plugin ${name} in config`);
38
- }
39
- }
40
- }
54
+ // Create plugins from config
55
+ const plugins = createPlugins(config.plugins, customPluginMap);
41
56
  log(`[Grafema MCP] Total plugins: ${plugins.length}`);
42
57
  // Check for parallel analysis config
43
58
  const parallelConfig = config.analysis?.parallel;
@@ -75,9 +90,13 @@ export async function ensureAnalyzed(serviceName = null) {
75
90
  total: parseFloat(totalTime),
76
91
  },
77
92
  });
78
- log(`[Grafema MCP] Analysis complete in ${totalTime}s`);
93
+ log(`[Grafema MCP] Analysis complete in ${totalTime}s`);
94
+ return db;
95
+ }
96
+ finally {
97
+ // ALWAYS release the lock, even on error
98
+ releaseLock();
79
99
  }
80
- return db;
81
100
  }
82
101
  /**
83
102
  * Discover services without running full analysis
@@ -95,7 +114,7 @@ export async function discoverServices() {
95
114
  ])),
96
115
  };
97
116
  const plugins = [];
98
- const discoveryPluginNames = config.plugins?.discovery || [];
117
+ const discoveryPluginNames = config.plugins.discovery ?? [];
99
118
  for (const name of discoveryPluginNames) {
100
119
  const factory = availablePlugins[name];
101
120
  if (factory) {
package/dist/config.d.ts CHANGED
@@ -1,16 +1,12 @@
1
1
  /**
2
2
  * MCP Server Configuration
3
3
  */
4
- export interface PluginConfig {
5
- indexing: string[];
6
- analysis: string[];
7
- enrichment: string[];
8
- validation: string[];
9
- discovery?: string[];
10
- }
11
- export interface ProjectConfig {
12
- plugins: PluginConfig;
13
- discovery: {
4
+ import { type GrafemaConfig } from '@grafema/core';
5
+ /**
6
+ * MCP-specific configuration extends GrafemaConfig with additional fields.
7
+ */
8
+ export interface MCPConfig extends GrafemaConfig {
9
+ discovery?: {
14
10
  enabled: boolean;
15
11
  customOnly: boolean;
16
12
  };
@@ -20,15 +16,18 @@ export interface ProjectConfig {
20
16
  backend?: 'local' | 'rfdb';
21
17
  rfdb_socket?: string;
22
18
  }
23
- export declare const DEFAULT_CONFIG: ProjectConfig;
24
19
  type PluginFactory = () => unknown;
25
20
  export declare const BUILTIN_PLUGINS: Record<string, PluginFactory>;
26
- export declare function loadConfig(projectPath: string): ProjectConfig;
21
+ /**
22
+ * Load MCP configuration (extends base GrafemaConfig).
23
+ * Uses shared ConfigLoader but adds MCP-specific defaults.
24
+ */
25
+ export declare function loadConfig(projectPath: string): MCPConfig;
27
26
  export interface CustomPluginResult {
28
27
  plugins: unknown[];
29
28
  pluginMap: Record<string, new () => unknown>;
30
29
  }
31
30
  export declare function loadCustomPlugins(projectPath: string): Promise<CustomPluginResult>;
32
- export declare function createPlugins(pluginNames: string[], customPluginMap?: Record<string, new () => unknown>): unknown[];
31
+ export declare function createPlugins(config: GrafemaConfig['plugins'], customPluginMap?: Record<string, new () => unknown>): unknown[];
33
32
  export {};
34
33
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AA0CH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,YAAY,CAAC;IACtB,SAAS,EAAE;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,UAAU,EAAE,OAAO,CAAC;KACrB,CAAC;IACF,QAAQ,CAAC,EAAE;QACT,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,cAAc,EAAE,aAiC5B,CAAC;AAGF,KAAK,aAAa,GAAG,MAAM,OAAO,CAAC;AAEnC,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAiCzD,CAAC;AAGF,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,CA2B7D;AAGD,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,EAAE,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,OAAO,CAAC,CAAC;CAC9C;AAED,wBAAsB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAoCxF;AAGD,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EAAE,EACrB,eAAe,GAAE,MAAM,CAAC,MAAM,EAAE,UAAU,OAAO,CAAM,GACtD,OAAO,EAAE,CAuBX"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,EAAoC,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAoCrF;;GAEG;AACH,MAAM,WAAW,SAAU,SAAQ,aAAa;IAC9C,SAAS,CAAC,EAAE;QACV,OAAO,EAAE,OAAO,CAAC;QACjB,UAAU,EAAE,OAAO,CAAC;KACrB,CAAC;IACF,QAAQ,CAAC,EAAE;QACT,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAUD,KAAK,aAAa,GAAG,MAAM,OAAO,CAAC;AAEnC,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAiCzD,CAAC;AAGF;;;GAGG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAWzD;AAGD,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,EAAE,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,OAAO,CAAC,CAAC;CAC9C;AAED,wBAAsB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAoCxF;AAGD,wBAAgB,aAAa,CAC3B,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,EAChC,eAAe,GAAE,MAAM,CAAC,MAAM,EAAE,UAAU,OAAO,CAAM,GACtD,OAAO,EAAE,CA8BX"}
package/dist/config.js CHANGED
@@ -2,9 +2,10 @@
2
2
  * MCP Server Configuration
3
3
  */
4
4
  import { join } from 'path';
5
- import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
5
+ import { existsSync, readdirSync } from 'fs';
6
6
  import { pathToFileURL } from 'url';
7
7
  import { log } from './utils.js';
8
+ import { loadConfig as loadConfigFromCore } from '@grafema/core';
8
9
  // === PLUGIN IMPORTS ===
9
10
  import {
10
11
  // Indexing
@@ -15,35 +16,7 @@ JSASTAnalyzer, ExpressRouteAnalyzer, SocketIOAnalyzer, DatabaseAnalyzer, FetchAn
15
16
  MethodCallResolver, AliasTracker, ValueDomainAnalyzer, MountPointResolver, PrefixEvaluator, InstanceOfResolver, HTTPConnectionEnricher, RustFFIEnricher,
16
17
  // Validation
17
18
  CallResolverValidator, EvalBanValidator, SQLInjectionValidator, ShadowingDetector, GraphConnectivityValidator, DataFlowValidator, TypeScriptDeadCodeValidator, } from '@grafema/core';
18
- export const DEFAULT_CONFIG = {
19
- plugins: {
20
- indexing: ['JSModuleIndexer'],
21
- analysis: [
22
- 'JSASTAnalyzer',
23
- 'ExpressRouteAnalyzer',
24
- 'SocketIOAnalyzer',
25
- 'DatabaseAnalyzer',
26
- 'FetchAnalyzer',
27
- 'ServiceLayerAnalyzer',
28
- ],
29
- enrichment: [
30
- 'MethodCallResolver',
31
- 'AliasTracker',
32
- 'ValueDomainAnalyzer',
33
- 'MountPointResolver',
34
- 'PrefixEvaluator',
35
- 'HTTPConnectionEnricher',
36
- ],
37
- validation: [
38
- 'CallResolverValidator',
39
- 'EvalBanValidator',
40
- 'SQLInjectionValidator',
41
- 'ShadowingDetector',
42
- 'GraphConnectivityValidator',
43
- 'DataFlowValidator',
44
- 'TypeScriptDeadCodeValidator',
45
- ],
46
- },
19
+ const MCP_DEFAULTS = {
47
20
  discovery: {
48
21
  enabled: true,
49
22
  customOnly: false,
@@ -81,33 +54,20 @@ export const BUILTIN_PLUGINS = {
81
54
  TypeScriptDeadCodeValidator: () => new TypeScriptDeadCodeValidator(),
82
55
  };
83
56
  // === CONFIG LOADING ===
57
+ /**
58
+ * Load MCP configuration (extends base GrafemaConfig).
59
+ * Uses shared ConfigLoader but adds MCP-specific defaults.
60
+ */
84
61
  export function loadConfig(projectPath) {
85
- const configPath = join(projectPath, '.grafema', 'config.json');
86
- if (!existsSync(configPath)) {
87
- try {
88
- const grafemaDir = join(projectPath, '.grafema');
89
- if (!existsSync(grafemaDir)) {
90
- const { mkdirSync } = require('fs');
91
- mkdirSync(grafemaDir, { recursive: true });
92
- }
93
- writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2));
94
- log(`[Grafema MCP] Created default config: ${configPath}`);
95
- }
96
- catch (err) {
97
- log(`[Grafema MCP] Failed to create config: ${err.message}`);
98
- }
99
- return DEFAULT_CONFIG;
100
- }
101
- try {
102
- const configContent = readFileSync(configPath, 'utf-8');
103
- const config = JSON.parse(configContent);
104
- log(`[Grafema MCP] Loaded config from ${configPath}`);
105
- return { ...DEFAULT_CONFIG, ...config };
106
- }
107
- catch (err) {
108
- log(`[Grafema MCP] Failed to load config: ${err.message}, using defaults`);
109
- return DEFAULT_CONFIG;
110
- }
62
+ // Use shared loader (handles YAML/JSON, deprecation warnings)
63
+ const baseConfig = loadConfigFromCore(projectPath, {
64
+ warn: (msg) => log(`[Grafema MCP] ${msg}`),
65
+ });
66
+ // Add MCP-specific defaults
67
+ return {
68
+ ...baseConfig,
69
+ ...MCP_DEFAULTS,
70
+ };
111
71
  }
112
72
  export async function loadCustomPlugins(projectPath) {
113
73
  const pluginsDir = join(projectPath, '.grafema', 'plugins');
@@ -142,7 +102,13 @@ export async function loadCustomPlugins(projectPath) {
142
102
  return { plugins: customPlugins, pluginMap };
143
103
  }
144
104
  // === PLUGIN INSTANTIATION ===
145
- export function createPlugins(pluginNames, customPluginMap = {}) {
105
+ export function createPlugins(config, customPluginMap = {}) {
106
+ const pluginNames = [
107
+ ...config.indexing,
108
+ ...config.analysis,
109
+ ...config.enrichment,
110
+ ...config.validation,
111
+ ];
146
112
  const plugins = [];
147
113
  const availablePlugins = {
148
114
  ...BUILTIN_PLUGINS,
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../src/handlers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAcH,OAAO,KAAK,EACV,UAAU,EACV,cAAc,EACd,aAAa,EACb,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,eAAe,EACf,oBAAoB,EAGrB,MAAM,YAAY,CAAC;AAKpB,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CA8EhF;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CA2E9E;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAiD9E;AAID,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CA2DhF;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CA+DtF;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,UAAU,CAAC,CA4CxF;AAID,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,UAAU,CAAC,CAoBxF;AAED,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,UAAU,CAAC,CAYnE;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,UAAU,CAAC,CAe1D;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAwB9E;AAID;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CA6D1F;AAED;;GAEG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,UAAU,CAAC,CAyChE;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAwG1F;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CA8B1F;AAID,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAalF;AAED,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,UAAU,CAAC,CAwE5F;AAID,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,OAAO,YAAY,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAkEvG"}
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../src/handlers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAeH,OAAO,KAAK,EACV,UAAU,EACV,cAAc,EACd,aAAa,EACb,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,eAAe,EACf,oBAAoB,EAGrB,MAAM,YAAY,CAAC;AAKpB,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CA8EhF;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CA2E9E;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAiD9E;AAID,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CA2DhF;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAC,CA+DtF;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,UAAU,CAAC,CA4CxF;AAID,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,UAAU,CAAC,CA4BxF;AAED,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,UAAU,CAAC,CAYnE;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,UAAU,CAAC,CAe1D;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAwB9E;AAID;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CA6D1F;AAED;;GAEG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,UAAU,CAAC,CAyChE;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAwG1F;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CA8B1F;AAID,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAqClF;AAED,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,UAAU,CAAC,CAwE5F;AAID,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,OAAO,YAAY,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAkEvG"}
package/dist/handlers.js CHANGED
@@ -2,7 +2,8 @@
2
2
  * MCP Tool Handlers
3
3
  */
4
4
  import { ensureAnalyzed } from './analysis.js';
5
- import { getProjectPath, getAnalysisStatus, setIsAnalyzed, getOrCreateBackend, getGuaranteeManager, getGuaranteeAPI } from './state.js';
5
+ import { getProjectPath, getAnalysisStatus, getOrCreateBackend, getGuaranteeManager, getGuaranteeAPI, isAnalysisRunning } from './state.js';
6
+ import { CoverageAnalyzer } from '@grafema/core';
6
7
  import { normalizeLimit, formatPaginationInfo, guardResponseSize, serializeBigInt, findSimilarTypes, textResult, errorResult, } from './utils.js';
7
8
  import { isGuaranteeType } from '@grafema/core';
8
9
  // === QUERY HANDLERS ===
@@ -174,7 +175,7 @@ export async function handleFindNodes(args) {
174
175
  // === TRACE HANDLERS ===
175
176
  export async function handleTraceAlias(args) {
176
177
  const db = await ensureAnalyzed();
177
- const { identifier: variableName, file } = args;
178
+ const { variableName, file } = args;
178
179
  const projectPath = getProjectPath();
179
180
  let varNode = null;
180
181
  for await (const node of db.queryNodes({ type: 'VARIABLE' })) {
@@ -306,13 +307,18 @@ export async function handleCheckInvariant(args) {
306
307
  // === ANALYSIS HANDLERS ===
307
308
  export async function handleAnalyzeProject(args) {
308
309
  const { service, force } = args;
309
- if (force) {
310
- setIsAnalyzed(false);
310
+ // Early check: return error for force=true if analysis is already running
311
+ // This provides immediate feedback instead of waiting or causing corruption
312
+ if (force && isAnalysisRunning()) {
313
+ return errorResult('Cannot force re-analysis: analysis is already in progress. ' +
314
+ 'Use get_analysis_status to check current status, or wait for completion.');
311
315
  }
316
+ // Note: setIsAnalyzed(false) is now handled inside ensureAnalyzed() within the lock
317
+ // to prevent race conditions where multiple calls could both clear the database
312
318
  try {
313
- await ensureAnalyzed(service || null);
319
+ await ensureAnalyzed(service || null, force || false);
314
320
  const status = getAnalysisStatus();
315
- return textResult(`✅ Analysis complete!\n` +
321
+ return textResult(`Analysis complete!\n` +
316
322
  `- Services discovered: ${status.servicesDiscovered}\n` +
317
323
  `- Services analyzed: ${status.servicesAnalyzed}\n` +
318
324
  `- Total time: ${status.timings.total || 'N/A'}s`);
@@ -608,11 +614,34 @@ export async function handleGetCoverage(args) {
608
614
  const db = await getOrCreateBackend();
609
615
  const projectPath = getProjectPath();
610
616
  const { path: targetPath = projectPath } = args;
611
- const nodeCount = await db.nodeCount();
612
- const moduleNodes = db.findByType ? await db.findByType('MODULE') : [];
613
- return textResult(`Coverage for ${targetPath}:\n` +
614
- `- Analyzed files: ${moduleNodes.length}\n` +
615
- `- Total nodes: ${nodeCount}\n`);
617
+ try {
618
+ const analyzer = new CoverageAnalyzer(db, targetPath);
619
+ const result = await analyzer.analyze();
620
+ // Format output for AI agents
621
+ let output = `Analysis Coverage for ${targetPath}\n`;
622
+ output += `==============================\n\n`;
623
+ output += `File breakdown:\n`;
624
+ output += ` Total files: ${result.total}\n`;
625
+ output += ` Analyzed: ${result.analyzed.count} (${result.percentages.analyzed}%) - in graph\n`;
626
+ output += ` Unsupported: ${result.unsupported.count} (${result.percentages.unsupported}%) - no indexer available\n`;
627
+ output += ` Unreachable: ${result.unreachable.count} (${result.percentages.unreachable}%) - not imported from entrypoints\n`;
628
+ if (result.unsupported.count > 0) {
629
+ output += `\nUnsupported files by extension:\n`;
630
+ for (const [ext, files] of Object.entries(result.unsupported.byExtension)) {
631
+ output += ` ${ext}: ${files.length} files\n`;
632
+ }
633
+ }
634
+ if (result.unreachable.count > 0) {
635
+ output += `\nUnreachable source files:\n`;
636
+ for (const [ext, files] of Object.entries(result.unreachable.byExtension)) {
637
+ output += ` ${ext}: ${files.length} files\n`;
638
+ }
639
+ }
640
+ return textResult(output);
641
+ }
642
+ catch (error) {
643
+ return errorResult(`Failed to calculate coverage: ${error.message}`);
644
+ }
616
645
  }
617
646
  export async function handleGetDocumentation(args) {
618
647
  const { topic = 'overview' } = args;
package/dist/state.d.ts CHANGED
@@ -15,6 +15,45 @@ export declare function setIsAnalyzed(value: boolean): void;
15
15
  export declare function setAnalysisStatus(status: Partial<AnalysisStatus>): void;
16
16
  export declare function setBackgroundPid(pid: number | null): void;
17
17
  export declare function updateAnalysisTimings(timings: Partial<AnalysisStatus['timings']>): void;
18
+ /**
19
+ * Check if analysis is currently running.
20
+ *
21
+ * Use this to check status before attempting operations that conflict
22
+ * with analysis (e.g., force re-analysis while analysis is in progress).
23
+ *
24
+ * @returns true if analysis is in progress, false otherwise
25
+ */
26
+ export declare function isAnalysisRunning(): boolean;
27
+ /**
28
+ * Acquire the analysis lock.
29
+ *
30
+ * This function implements a Promise-based mutex for serializing analysis operations.
31
+ * Only one analysis can run at a time. If another analysis is running, this function
32
+ * waits for it to complete (up to LOCK_TIMEOUT_MS).
33
+ *
34
+ * Usage:
35
+ * ```typescript
36
+ * const releaseLock = await acquireAnalysisLock();
37
+ * try {
38
+ * // ... perform analysis ...
39
+ * } finally {
40
+ * releaseLock();
41
+ * }
42
+ * ```
43
+ *
44
+ * @returns A release function to call when analysis is complete
45
+ * @throws Error if timeout (10 minutes) expires while waiting for existing analysis
46
+ */
47
+ export declare function acquireAnalysisLock(): Promise<() => void>;
48
+ /**
49
+ * Wait for any running analysis to complete without acquiring the lock.
50
+ *
51
+ * Use this when you need to wait for analysis completion but don't need
52
+ * to start a new analysis yourself.
53
+ *
54
+ * @returns Promise that resolves when no analysis is running
55
+ */
56
+ export declare function waitForAnalysis(): Promise<void>;
18
57
  export declare function getOrCreateBackend(): Promise<GraphBackend>;
19
58
  export declare function getBackendIfExists(): GraphBackend | null;
20
59
  export declare function setupLogging(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAqB,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAIlF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAgCnD,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED,wBAAgB,iBAAiB,IAAI,cAAc,CAElD;AAED,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD;AAED,wBAAgB,mBAAmB,IAAI,gBAAgB,GAAG,IAAI,CAE7D;AAED,wBAAgB,eAAe,IAAI,YAAY,GAAG,IAAI,CAErD;AAGD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEjD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAElD;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,IAAI,CAEvE;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEzD;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,CAEvF;AAGD,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,YAAY,CAAC,CAgChE;AAkBD,wBAAgB,kBAAkB,IAAI,YAAY,GAAG,IAAI,CAExD;AAGD,wBAAgB,YAAY,IAAI,IAAI,CAMnC;AAGD,wBAAgB,kBAAkB,IAAI,IAAI,CAQzC;AAGD,wBAAsB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAI7C"}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAqB,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAIlF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AA+EnD,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED,wBAAgB,iBAAiB,IAAI,cAAc,CAElD;AAED,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD;AAED,wBAAgB,mBAAmB,IAAI,gBAAgB,GAAG,IAAI,CAE7D;AAED,wBAAgB,eAAe,IAAI,YAAY,GAAG,IAAI,CAErD;AAGD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEjD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAElD;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,IAAI,CAEvE;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEzD;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,CAEvF;AAID;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC,CA8B/D;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAIrD;AAGD,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,YAAY,CAAC,CAgChE;AAkBD,wBAAgB,kBAAkB,IAAI,YAAY,GAAG,IAAI,CAExD;AAGD,wBAAgB,YAAY,IAAI,IAAI,CAMnC;AAGD,wBAAgB,kBAAkB,IAAI,IAAI,CAQzC;AAGD,wBAAsB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAI7C"}
package/dist/state.js CHANGED
@@ -32,6 +32,51 @@ let analysisStatus = {
32
32
  total: null,
33
33
  },
34
34
  };
35
+ // === ANALYSIS LOCK ===
36
+ //
37
+ // Promise-based mutex for analysis serialization.
38
+ //
39
+ // Why not a simple boolean flag?
40
+ // - Boolean can indicate "analysis is running" but cannot make callers wait
41
+ // - Promise allows awaiting until analysis completes
42
+ //
43
+ // Pattern:
44
+ // - null = no analysis running, lock available
45
+ // - Promise = analysis running, await it to wait for completion
46
+ //
47
+ // Behavior on force=true during analysis:
48
+ // - Returns error immediately (does NOT wait)
49
+ // - Rationale: force=true implies "clear DB and re-analyze"
50
+ // - Clearing DB while another analysis writes = corruption
51
+ // - Better UX: immediate feedback vs mysterious wait
52
+ //
53
+ // Scope: Global Lock (not per-service) because:
54
+ // - Single RFDB backend instance
55
+ // - db.clear() affects entire database
56
+ // - Simpler reasoning about state
57
+ //
58
+ // Process Death Behavior:
59
+ // - Lock is in-memory - next process starts with fresh state (no deadlock)
60
+ // - RFDB may have partial data from incomplete analysis
61
+ // - isAnalyzed resets to false - next call will re-analyze
62
+ // - RFDB is append-only - partial data won't corrupt existing data
63
+ //
64
+ // Worker Process Coordination:
65
+ // - Worker is SEPARATE process from MCP server
66
+ // - MCP server calls db.clear() INSIDE the lock, BEFORE spawning worker
67
+ // - Worker assumes DB is already clean and does NOT call clear()
68
+ //
69
+ // Timeout:
70
+ // - Lock acquisition times out after 10 minutes
71
+ // - Matches project's execution guard policy (see CLAUDE.md)
72
+ //
73
+ let analysisLock = null;
74
+ let analysisLockResolve = null;
75
+ /**
76
+ * Lock timeout in milliseconds (10 minutes).
77
+ * Matches project's execution guard policy - max 10 minutes for any operation.
78
+ */
79
+ const LOCK_TIMEOUT_MS = 10 * 60 * 1000;
35
80
  // === GETTERS ===
36
81
  export function getProjectPath() {
37
82
  return projectPath;
@@ -67,6 +112,76 @@ export function setBackgroundPid(pid) {
67
112
  export function updateAnalysisTimings(timings) {
68
113
  analysisStatus.timings = { ...analysisStatus.timings, ...timings };
69
114
  }
115
+ // === ANALYSIS LOCK FUNCTIONS ===
116
+ /**
117
+ * Check if analysis is currently running.
118
+ *
119
+ * Use this to check status before attempting operations that conflict
120
+ * with analysis (e.g., force re-analysis while analysis is in progress).
121
+ *
122
+ * @returns true if analysis is in progress, false otherwise
123
+ */
124
+ export function isAnalysisRunning() {
125
+ return analysisLock !== null;
126
+ }
127
+ /**
128
+ * Acquire the analysis lock.
129
+ *
130
+ * This function implements a Promise-based mutex for serializing analysis operations.
131
+ * Only one analysis can run at a time. If another analysis is running, this function
132
+ * waits for it to complete (up to LOCK_TIMEOUT_MS).
133
+ *
134
+ * Usage:
135
+ * ```typescript
136
+ * const releaseLock = await acquireAnalysisLock();
137
+ * try {
138
+ * // ... perform analysis ...
139
+ * } finally {
140
+ * releaseLock();
141
+ * }
142
+ * ```
143
+ *
144
+ * @returns A release function to call when analysis is complete
145
+ * @throws Error if timeout (10 minutes) expires while waiting for existing analysis
146
+ */
147
+ export async function acquireAnalysisLock() {
148
+ const start = Date.now();
149
+ // Wait for any existing analysis to complete (with timeout)
150
+ while (analysisLock !== null) {
151
+ if (Date.now() - start > LOCK_TIMEOUT_MS) {
152
+ throw new Error('Analysis lock timeout (10 minutes). Previous analysis may have failed. ' +
153
+ 'Check .grafema/mcp.log for errors or restart MCP server.');
154
+ }
155
+ await analysisLock;
156
+ }
157
+ // Create new lock - a Promise that will be resolved when analysis completes
158
+ analysisLock = new Promise((resolve) => {
159
+ analysisLockResolve = resolve;
160
+ });
161
+ // Update status to reflect that analysis is running
162
+ setAnalysisStatus({ running: true });
163
+ // Return release function
164
+ return () => {
165
+ setAnalysisStatus({ running: false });
166
+ const resolve = analysisLockResolve;
167
+ analysisLock = null;
168
+ analysisLockResolve = null;
169
+ resolve?.();
170
+ };
171
+ }
172
+ /**
173
+ * Wait for any running analysis to complete without acquiring the lock.
174
+ *
175
+ * Use this when you need to wait for analysis completion but don't need
176
+ * to start a new analysis yourself.
177
+ *
178
+ * @returns Promise that resolves when no analysis is running
179
+ */
180
+ export async function waitForAnalysis() {
181
+ if (analysisLock) {
182
+ await analysisLock;
183
+ }
184
+ }
70
185
  // === BACKEND ===
71
186
  export async function getOrCreateBackend() {
72
187
  if (backend)
package/dist/types.d.ts CHANGED
@@ -28,13 +28,8 @@ export interface PaginationParams {
28
28
  total?: number;
29
29
  hasMore: boolean;
30
30
  }
31
- export interface GrafemaConfig {
32
- project_path?: string;
33
- backend?: 'local' | 'rfdb';
34
- rfdb_socket?: string;
35
- plugins?: string[];
36
- ignore_patterns?: string[];
37
- }
31
+ export type { GrafemaConfig } from '@grafema/core';
32
+ export type { MCPConfig } from './config.js';
38
33
  export interface QueryGraphArgs {
39
34
  query: string;
40
35
  limit?: number;
@@ -48,7 +43,7 @@ export interface FindCallsArgs {
48
43
  include_indirect?: boolean;
49
44
  }
50
45
  export interface TraceAliasArgs {
51
- identifier: string;
46
+ variableName: string;
52
47
  file?: string;
53
48
  max_depth?: number;
54
49
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,IAAI,CAAC;AAGtC,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,eAAe,CAAC;CAC1B;AAGD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAGD,MAAM,WAAW,aAAa;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAGD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,CAAC;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAKD,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAGlF,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,UAAU,GAAG,QAAQ,GAAG,UAAU,GAAG,YAAY,CAAC;AAE/F,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IAEb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IAExC,IAAI,CAAC,EAAE,iBAAiB,GAAG,eAAe,GAAG,sBAAsB,CAAC;IACpE,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,MAAM,CAAC;CACd;AAGD,MAAM,WAAW,UAAU;IACzB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACrB,OAAO,EAAE,KAAK,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;KACd,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAGD,MAAM,WAAW,YAAY;IAC3B,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7B,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7B,gBAAgB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3E,gBAAgB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3E,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAC/C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5C,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9D,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAC5E,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAC5E,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;IACtE,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACpE,eAAe,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IACpD,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAGD,MAAM,WAAW,QAAQ;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,YAAY,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;IAC/B,SAAS,EAAE,WAAW,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAGD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,WAAW,GAAG,OAAO,CAAC;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAGD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafema/mcp",
3
- "version": "0.1.0-alpha.5",
3
+ "version": "0.1.1-alpha",
4
4
  "description": "MCP server for Grafema code analysis toolkit",
5
5
  "type": "module",
6
6
  "main": "./dist/server.js",
@@ -38,16 +38,19 @@
38
38
  "dependencies": {
39
39
  "@modelcontextprotocol/sdk": "^1.25.1",
40
40
  "ajv": "^8.17.1",
41
- "@grafema/core": "0.1.0-alpha.5",
42
- "@grafema/types": "0.1.0-alpha.5"
41
+ "@grafema/types": "0.1.1-alpha",
42
+ "@grafema/core": "0.1.1-alpha"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^25.0.8",
46
+ "tsx": "^4.19.2",
46
47
  "typescript": "^5.9.3"
47
48
  },
48
49
  "scripts": {
49
50
  "build": "tsc",
50
51
  "clean": "rm -rf dist",
51
- "start": "node dist/server.js"
52
+ "start": "node dist/server.js",
53
+ "test": "node --import tsx --test test/*.test.ts",
54
+ "test:watch": "node --import tsx --test --watch test/*.test.ts"
52
55
  }
53
56
  }
@@ -213,7 +213,12 @@ async function run(): Promise<void> {
213
213
  console.log(`[Worker] Connecting to RFDB server: socket=${socketPath}, db=${dbPath}`);
214
214
  db = new RFDBServerBackend({ socketPath, dbPath });
215
215
  await db.connect();
216
- await db.clear();
216
+
217
+ // NOTE: db.clear() is NOT called here.
218
+ // MCP server clears DB INSIDE the analysis lock BEFORE spawning this worker.
219
+ // This prevents race conditions where concurrent analysis calls could both
220
+ // clear the database. Worker assumes DB is already clean.
221
+ // See: REG-159 implementation, Phase 2.5 (Worker Clear Coordination)
217
222
 
218
223
  sendProgress({ phase: 'discovery', message: 'Starting analysis...' });
219
224
 
package/src/analysis.ts CHANGED
@@ -10,20 +10,67 @@ import {
10
10
  setIsAnalyzed,
11
11
  getAnalysisStatus,
12
12
  setAnalysisStatus,
13
+ isAnalysisRunning,
14
+ acquireAnalysisLock,
13
15
  } from './state.js';
14
- import { loadConfig, loadCustomPlugins, BUILTIN_PLUGINS } from './config.js';
16
+ import { loadConfig, loadCustomPlugins, createPlugins } from './config.js';
15
17
  import { log } from './utils.js';
16
18
  import type { GraphBackend } from '@grafema/types';
17
19
 
18
20
  /**
19
- * Ensure project is analyzed, optionally filtering to a single service
21
+ * Ensure project is analyzed, optionally filtering to a single service.
22
+ *
23
+ * CONCURRENCY: This function is protected by a global mutex.
24
+ * - Only one analysis can run at a time
25
+ * - Concurrent calls wait for the current analysis to complete
26
+ * - force=true while analysis is running returns an error immediately
27
+ *
28
+ * @param serviceName - Optional service to analyze (null = all)
29
+ * @param force - If true, clear DB and re-analyze even if already analyzed.
30
+ * ERROR if another analysis is already running.
31
+ * @throws Error if force=true and analysis is already running
20
32
  */
21
- export async function ensureAnalyzed(serviceName: string | null = null): Promise<GraphBackend> {
33
+ export async function ensureAnalyzed(
34
+ serviceName: string | null = null,
35
+ force: boolean = false
36
+ ): Promise<GraphBackend> {
22
37
  const db = await getOrCreateBackend();
23
38
  const projectPath = getProjectPath();
24
- const isAnalyzed = getIsAnalyzed();
25
39
 
26
- if (!isAnalyzed || serviceName) {
40
+ // CONCURRENCY CHECK: If force=true and analysis is running, error immediately
41
+ // This check is BEFORE acquiring lock to fail fast
42
+ if (force && isAnalysisRunning()) {
43
+ throw new Error(
44
+ 'Analysis is already in progress. Cannot force re-analysis while another analysis is running. ' +
45
+ 'Wait for the current analysis to complete or check status with get_analysis_status.'
46
+ );
47
+ }
48
+
49
+ // Skip if already analyzed (and not forcing, and no service filter)
50
+ if (getIsAnalyzed() && !serviceName && !force) {
51
+ return db;
52
+ }
53
+
54
+ // Acquire lock (waits if another analysis is running)
55
+ const releaseLock = await acquireAnalysisLock();
56
+
57
+ try {
58
+ // Double-check after acquiring lock (another call might have completed analysis while we waited)
59
+ if (getIsAnalyzed() && !serviceName && !force) {
60
+ return db;
61
+ }
62
+
63
+ // Clear DB inside lock, BEFORE running analysis
64
+ // This is critical for worker coordination: MCP server clears DB here,
65
+ // worker does NOT call db.clear() (see analysis-worker.ts)
66
+ if (force || !getIsAnalyzed()) {
67
+ log('[Grafema MCP] Clearing database before analysis...');
68
+ if (db.clear) {
69
+ await db.clear();
70
+ }
71
+ setIsAnalyzed(false);
72
+ }
73
+
27
74
  log(
28
75
  `[Grafema MCP] Analyzing project: ${projectPath}${serviceName ? ` (service: ${serviceName})` : ''}`
29
76
  );
@@ -31,30 +78,8 @@ export async function ensureAnalyzed(serviceName: string | null = null): Promise
31
78
  const config = loadConfig(projectPath);
32
79
  const { pluginMap: customPluginMap } = await loadCustomPlugins(projectPath);
33
80
 
34
- // Merge builtin and custom plugins
35
- const availablePlugins: Record<string, () => unknown> = {
36
- ...BUILTIN_PLUGINS,
37
- ...Object.fromEntries(
38
- Object.entries(customPluginMap).map(([name, PluginClass]) => [
39
- name,
40
- () => new PluginClass(),
41
- ])
42
- ),
43
- };
44
-
45
- // Build plugin list from config
46
- const plugins: unknown[] = [];
47
- for (const [phase, pluginNames] of Object.entries(config.plugins || {})) {
48
- for (const name of pluginNames as string[]) {
49
- const factory = availablePlugins[name];
50
- if (factory) {
51
- plugins.push(factory());
52
- log(`[Grafema MCP] Enabled plugin: ${name} (${phase})`);
53
- } else {
54
- log(`[Grafema MCP] Warning: Unknown plugin ${name} in config`);
55
- }
56
- }
57
- }
81
+ // Create plugins from config
82
+ const plugins = createPlugins(config.plugins, customPluginMap);
58
83
 
59
84
  log(`[Grafema MCP] Total plugins: ${plugins.length}`);
60
85
 
@@ -105,10 +130,13 @@ export async function ensureAnalyzed(serviceName: string | null = null): Promise
105
130
  },
106
131
  });
107
132
 
108
- log(`[Grafema MCP] Analysis complete in ${totalTime}s`);
109
- }
133
+ log(`[Grafema MCP] Analysis complete in ${totalTime}s`);
110
134
 
111
- return db;
135
+ return db;
136
+ } finally {
137
+ // ALWAYS release the lock, even on error
138
+ releaseLock();
139
+ }
112
140
  }
113
141
 
114
142
  /**
@@ -133,7 +161,7 @@ export async function discoverServices(): Promise<unknown[]> {
133
161
  };
134
162
 
135
163
  const plugins: unknown[] = [];
136
- const discoveryPluginNames = (config.plugins as any)?.discovery || [];
164
+ const discoveryPluginNames = config.plugins.discovery ?? [];
137
165
 
138
166
  for (const name of discoveryPluginNames) {
139
167
  const factory = availablePlugins[name];
package/src/config.ts CHANGED
@@ -3,10 +3,10 @@
3
3
  */
4
4
 
5
5
  import { join } from 'path';
6
- import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
6
+ import { existsSync, readdirSync } from 'fs';
7
7
  import { pathToFileURL } from 'url';
8
8
  import { log } from './utils.js';
9
- import type { GrafemaConfig } from './types.js';
9
+ import { loadConfig as loadConfigFromCore, type GrafemaConfig } from '@grafema/core';
10
10
 
11
11
  // === PLUGIN IMPORTS ===
12
12
  import {
@@ -41,18 +41,12 @@ import {
41
41
  TypeScriptDeadCodeValidator,
42
42
  } from '@grafema/core';
43
43
 
44
- // === DEFAULT CONFIG ===
45
- export interface PluginConfig {
46
- indexing: string[];
47
- analysis: string[];
48
- enrichment: string[];
49
- validation: string[];
50
- discovery?: string[];
51
- }
52
-
53
- export interface ProjectConfig {
54
- plugins: PluginConfig;
55
- discovery: {
44
+ // === MCP-SPECIFIC CONFIG ===
45
+ /**
46
+ * MCP-specific configuration extends GrafemaConfig with additional fields.
47
+ */
48
+ export interface MCPConfig extends GrafemaConfig {
49
+ discovery?: {
56
50
  enabled: boolean;
57
51
  customOnly: boolean;
58
52
  };
@@ -63,35 +57,7 @@ export interface ProjectConfig {
63
57
  rfdb_socket?: string;
64
58
  }
65
59
 
66
- export const DEFAULT_CONFIG: ProjectConfig = {
67
- plugins: {
68
- indexing: ['JSModuleIndexer'],
69
- analysis: [
70
- 'JSASTAnalyzer',
71
- 'ExpressRouteAnalyzer',
72
- 'SocketIOAnalyzer',
73
- 'DatabaseAnalyzer',
74
- 'FetchAnalyzer',
75
- 'ServiceLayerAnalyzer',
76
- ],
77
- enrichment: [
78
- 'MethodCallResolver',
79
- 'AliasTracker',
80
- 'ValueDomainAnalyzer',
81
- 'MountPointResolver',
82
- 'PrefixEvaluator',
83
- 'HTTPConnectionEnricher',
84
- ],
85
- validation: [
86
- 'CallResolverValidator',
87
- 'EvalBanValidator',
88
- 'SQLInjectionValidator',
89
- 'ShadowingDetector',
90
- 'GraphConnectivityValidator',
91
- 'DataFlowValidator',
92
- 'TypeScriptDeadCodeValidator',
93
- ],
94
- },
60
+ const MCP_DEFAULTS: Pick<MCPConfig, 'discovery'> = {
95
61
  discovery: {
96
62
  enabled: true,
97
63
  customOnly: false,
@@ -137,33 +103,21 @@ export const BUILTIN_PLUGINS: Record<string, PluginFactory> = {
137
103
  };
138
104
 
139
105
  // === CONFIG LOADING ===
140
- export function loadConfig(projectPath: string): ProjectConfig {
141
- const configPath = join(projectPath, '.grafema', 'config.json');
142
-
143
- if (!existsSync(configPath)) {
144
- try {
145
- const grafemaDir = join(projectPath, '.grafema');
146
- if (!existsSync(grafemaDir)) {
147
- const { mkdirSync } = require('fs');
148
- mkdirSync(grafemaDir, { recursive: true });
149
- }
150
- writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2));
151
- log(`[Grafema MCP] Created default config: ${configPath}`);
152
- } catch (err) {
153
- log(`[Grafema MCP] Failed to create config: ${(err as Error).message}`);
154
- }
155
- return DEFAULT_CONFIG;
156
- }
157
-
158
- try {
159
- const configContent = readFileSync(configPath, 'utf-8');
160
- const config = JSON.parse(configContent) as Partial<ProjectConfig>;
161
- log(`[Grafema MCP] Loaded config from ${configPath}`);
162
- return { ...DEFAULT_CONFIG, ...config };
163
- } catch (err) {
164
- log(`[Grafema MCP] Failed to load config: ${(err as Error).message}, using defaults`);
165
- return DEFAULT_CONFIG;
166
- }
106
+ /**
107
+ * Load MCP configuration (extends base GrafemaConfig).
108
+ * Uses shared ConfigLoader but adds MCP-specific defaults.
109
+ */
110
+ export function loadConfig(projectPath: string): MCPConfig {
111
+ // Use shared loader (handles YAML/JSON, deprecation warnings)
112
+ const baseConfig = loadConfigFromCore(projectPath, {
113
+ warn: (msg) => log(`[Grafema MCP] ${msg}`),
114
+ });
115
+
116
+ // Add MCP-specific defaults
117
+ return {
118
+ ...baseConfig,
119
+ ...MCP_DEFAULTS,
120
+ };
167
121
  }
168
122
 
169
123
  // === CUSTOM PLUGINS ===
@@ -212,9 +166,16 @@ export async function loadCustomPlugins(projectPath: string): Promise<CustomPlug
212
166
 
213
167
  // === PLUGIN INSTANTIATION ===
214
168
  export function createPlugins(
215
- pluginNames: string[],
169
+ config: GrafemaConfig['plugins'],
216
170
  customPluginMap: Record<string, new () => unknown> = {}
217
171
  ): unknown[] {
172
+ const pluginNames = [
173
+ ...config.indexing,
174
+ ...config.analysis,
175
+ ...config.enrichment,
176
+ ...config.validation,
177
+ ];
178
+
218
179
  const plugins: unknown[] = [];
219
180
  const availablePlugins: Record<string, PluginFactory> = {
220
181
  ...BUILTIN_PLUGINS,
package/src/handlers.ts CHANGED
@@ -4,7 +4,8 @@
4
4
 
5
5
  import { join } from 'path';
6
6
  import { ensureAnalyzed } from './analysis.js';
7
- import { getProjectPath, getAnalysisStatus, setIsAnalyzed, getOrCreateBackend, getGuaranteeManager, getGuaranteeAPI } from './state.js';
7
+ import { getProjectPath, getAnalysisStatus, getOrCreateBackend, getGuaranteeManager, getGuaranteeAPI, isAnalysisRunning } from './state.js';
8
+ import { CoverageAnalyzer } from '@grafema/core';
8
9
  import {
9
10
  normalizeLimit,
10
11
  formatPaginationInfo,
@@ -248,7 +249,7 @@ export async function handleFindNodes(args: FindNodesArgs): Promise<ToolResult>
248
249
 
249
250
  export async function handleTraceAlias(args: TraceAliasArgs): Promise<ToolResult> {
250
251
  const db = await ensureAnalyzed();
251
- const { identifier: variableName, file } = args;
252
+ const { variableName, file } = args;
252
253
  const projectPath = getProjectPath();
253
254
 
254
255
  let varNode: GraphNode | null = null;
@@ -423,16 +424,24 @@ export async function handleCheckInvariant(args: CheckInvariantArgs): Promise<To
423
424
  export async function handleAnalyzeProject(args: AnalyzeProjectArgs): Promise<ToolResult> {
424
425
  const { service, force } = args;
425
426
 
426
- if (force) {
427
- setIsAnalyzed(false);
427
+ // Early check: return error for force=true if analysis is already running
428
+ // This provides immediate feedback instead of waiting or causing corruption
429
+ if (force && isAnalysisRunning()) {
430
+ return errorResult(
431
+ 'Cannot force re-analysis: analysis is already in progress. ' +
432
+ 'Use get_analysis_status to check current status, or wait for completion.'
433
+ );
428
434
  }
429
435
 
436
+ // Note: setIsAnalyzed(false) is now handled inside ensureAnalyzed() within the lock
437
+ // to prevent race conditions where multiple calls could both clear the database
438
+
430
439
  try {
431
- await ensureAnalyzed(service || null);
440
+ await ensureAnalyzed(service || null, force || false);
432
441
  const status = getAnalysisStatus();
433
442
 
434
443
  return textResult(
435
- `✅ Analysis complete!\n` +
444
+ `Analysis complete!\n` +
436
445
  `- Services discovered: ${status.servicesDiscovered}\n` +
437
446
  `- Services analyzed: ${status.servicesAnalyzed}\n` +
438
447
  `- Total time: ${status.timings.total || 'N/A'}s`
@@ -764,14 +773,38 @@ export async function handleGetCoverage(args: GetCoverageArgs): Promise<ToolResu
764
773
  const projectPath = getProjectPath();
765
774
  const { path: targetPath = projectPath } = args;
766
775
 
767
- const nodeCount = await db.nodeCount();
768
- const moduleNodes = db.findByType ? await db.findByType('MODULE') : [];
776
+ try {
777
+ const analyzer = new CoverageAnalyzer(db, targetPath);
778
+ const result = await analyzer.analyze();
779
+
780
+ // Format output for AI agents
781
+ let output = `Analysis Coverage for ${targetPath}\n`;
782
+ output += `==============================\n\n`;
783
+
784
+ output += `File breakdown:\n`;
785
+ output += ` Total files: ${result.total}\n`;
786
+ output += ` Analyzed: ${result.analyzed.count} (${result.percentages.analyzed}%) - in graph\n`;
787
+ output += ` Unsupported: ${result.unsupported.count} (${result.percentages.unsupported}%) - no indexer available\n`;
788
+ output += ` Unreachable: ${result.unreachable.count} (${result.percentages.unreachable}%) - not imported from entrypoints\n`;
789
+
790
+ if (result.unsupported.count > 0) {
791
+ output += `\nUnsupported files by extension:\n`;
792
+ for (const [ext, files] of Object.entries(result.unsupported.byExtension)) {
793
+ output += ` ${ext}: ${files.length} files\n`;
794
+ }
795
+ }
769
796
 
770
- return textResult(
771
- `Coverage for ${targetPath}:\n` +
772
- `- Analyzed files: ${moduleNodes.length}\n` +
773
- `- Total nodes: ${nodeCount}\n`
774
- );
797
+ if (result.unreachable.count > 0) {
798
+ output += `\nUnreachable source files:\n`;
799
+ for (const [ext, files] of Object.entries(result.unreachable.byExtension)) {
800
+ output += ` ${ext}: ${files.length} files\n`;
801
+ }
802
+ }
803
+
804
+ return textResult(output);
805
+ } catch (error) {
806
+ return errorResult(`Failed to calculate coverage: ${(error as Error).message}`);
807
+ }
775
808
  }
776
809
 
777
810
  export async function handleGetDocumentation(args: GetDocumentationArgs): Promise<ToolResult> {
package/src/state.ts CHANGED
@@ -40,6 +40,53 @@ let analysisStatus: AnalysisStatus = {
40
40
  },
41
41
  };
42
42
 
43
+ // === ANALYSIS LOCK ===
44
+ //
45
+ // Promise-based mutex for analysis serialization.
46
+ //
47
+ // Why not a simple boolean flag?
48
+ // - Boolean can indicate "analysis is running" but cannot make callers wait
49
+ // - Promise allows awaiting until analysis completes
50
+ //
51
+ // Pattern:
52
+ // - null = no analysis running, lock available
53
+ // - Promise = analysis running, await it to wait for completion
54
+ //
55
+ // Behavior on force=true during analysis:
56
+ // - Returns error immediately (does NOT wait)
57
+ // - Rationale: force=true implies "clear DB and re-analyze"
58
+ // - Clearing DB while another analysis writes = corruption
59
+ // - Better UX: immediate feedback vs mysterious wait
60
+ //
61
+ // Scope: Global Lock (not per-service) because:
62
+ // - Single RFDB backend instance
63
+ // - db.clear() affects entire database
64
+ // - Simpler reasoning about state
65
+ //
66
+ // Process Death Behavior:
67
+ // - Lock is in-memory - next process starts with fresh state (no deadlock)
68
+ // - RFDB may have partial data from incomplete analysis
69
+ // - isAnalyzed resets to false - next call will re-analyze
70
+ // - RFDB is append-only - partial data won't corrupt existing data
71
+ //
72
+ // Worker Process Coordination:
73
+ // - Worker is SEPARATE process from MCP server
74
+ // - MCP server calls db.clear() INSIDE the lock, BEFORE spawning worker
75
+ // - Worker assumes DB is already clean and does NOT call clear()
76
+ //
77
+ // Timeout:
78
+ // - Lock acquisition times out after 10 minutes
79
+ // - Matches project's execution guard policy (see CLAUDE.md)
80
+ //
81
+ let analysisLock: Promise<void> | null = null;
82
+ let analysisLockResolve: (() => void) | null = null;
83
+
84
+ /**
85
+ * Lock timeout in milliseconds (10 minutes).
86
+ * Matches project's execution guard policy - max 10 minutes for any operation.
87
+ */
88
+ const LOCK_TIMEOUT_MS = 10 * 60 * 1000;
89
+
43
90
  // === GETTERS ===
44
91
  export function getProjectPath(): string {
45
92
  return projectPath;
@@ -86,6 +133,86 @@ export function updateAnalysisTimings(timings: Partial<AnalysisStatus['timings']
86
133
  analysisStatus.timings = { ...analysisStatus.timings, ...timings };
87
134
  }
88
135
 
136
+ // === ANALYSIS LOCK FUNCTIONS ===
137
+
138
+ /**
139
+ * Check if analysis is currently running.
140
+ *
141
+ * Use this to check status before attempting operations that conflict
142
+ * with analysis (e.g., force re-analysis while analysis is in progress).
143
+ *
144
+ * @returns true if analysis is in progress, false otherwise
145
+ */
146
+ export function isAnalysisRunning(): boolean {
147
+ return analysisLock !== null;
148
+ }
149
+
150
+ /**
151
+ * Acquire the analysis lock.
152
+ *
153
+ * This function implements a Promise-based mutex for serializing analysis operations.
154
+ * Only one analysis can run at a time. If another analysis is running, this function
155
+ * waits for it to complete (up to LOCK_TIMEOUT_MS).
156
+ *
157
+ * Usage:
158
+ * ```typescript
159
+ * const releaseLock = await acquireAnalysisLock();
160
+ * try {
161
+ * // ... perform analysis ...
162
+ * } finally {
163
+ * releaseLock();
164
+ * }
165
+ * ```
166
+ *
167
+ * @returns A release function to call when analysis is complete
168
+ * @throws Error if timeout (10 minutes) expires while waiting for existing analysis
169
+ */
170
+ export async function acquireAnalysisLock(): Promise<() => void> {
171
+ const start = Date.now();
172
+
173
+ // Wait for any existing analysis to complete (with timeout)
174
+ while (analysisLock !== null) {
175
+ if (Date.now() - start > LOCK_TIMEOUT_MS) {
176
+ throw new Error(
177
+ 'Analysis lock timeout (10 minutes). Previous analysis may have failed. ' +
178
+ 'Check .grafema/mcp.log for errors or restart MCP server.'
179
+ );
180
+ }
181
+ await analysisLock;
182
+ }
183
+
184
+ // Create new lock - a Promise that will be resolved when analysis completes
185
+ analysisLock = new Promise<void>((resolve) => {
186
+ analysisLockResolve = resolve;
187
+ });
188
+
189
+ // Update status to reflect that analysis is running
190
+ setAnalysisStatus({ running: true });
191
+
192
+ // Return release function
193
+ return () => {
194
+ setAnalysisStatus({ running: false });
195
+ const resolve = analysisLockResolve;
196
+ analysisLock = null;
197
+ analysisLockResolve = null;
198
+ resolve?.();
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Wait for any running analysis to complete without acquiring the lock.
204
+ *
205
+ * Use this when you need to wait for analysis completion but don't need
206
+ * to start a new analysis yourself.
207
+ *
208
+ * @returns Promise that resolves when no analysis is running
209
+ */
210
+ export async function waitForAnalysis(): Promise<void> {
211
+ if (analysisLock) {
212
+ await analysisLock;
213
+ }
214
+ }
215
+
89
216
  // === BACKEND ===
90
217
  export async function getOrCreateBackend(): Promise<GraphBackend> {
91
218
  if (backend) return backend;
package/src/types.ts CHANGED
@@ -36,13 +36,8 @@ export interface PaginationParams {
36
36
  }
37
37
 
38
38
  // === CONFIG ===
39
- export interface GrafemaConfig {
40
- project_path?: string;
41
- backend?: 'local' | 'rfdb';
42
- rfdb_socket?: string;
43
- plugins?: string[];
44
- ignore_patterns?: string[];
45
- }
39
+ export type { GrafemaConfig } from '@grafema/core';
40
+ export type { MCPConfig } from './config.js';
46
41
 
47
42
  // === TOOL ARGUMENTS ===
48
43
  export interface QueryGraphArgs {
@@ -60,7 +55,7 @@ export interface FindCallsArgs {
60
55
  }
61
56
 
62
57
  export interface TraceAliasArgs {
63
- identifier: string;
58
+ variableName: string;
64
59
  file?: string;
65
60
  max_depth?: number;
66
61
  }