code-graph-context 2.3.0 → 2.4.1

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.
package/README.md CHANGED
@@ -65,100 +65,90 @@ The system uses a dual-schema approach:
65
65
 
66
66
  Choose the installation method that works best for you:
67
67
 
68
- #### Option 1: Development Install (From Source)
68
+ #### Option 1: NPM Install (Recommended)
69
69
 
70
- Best for: Contributing to the project or customizing the code
71
-
72
- 1. **Clone the repository:**
73
70
  ```bash
74
- git clone https://github.com/drewdrewH/code-graph-context.git
75
- cd code-graph-context
76
- ```
71
+ # Install globally
72
+ npm install -g code-graph-context
77
73
 
78
- 2. **Install dependencies:**
79
- ```bash
80
- npm install
74
+ # Set up Neo4j (requires Docker)
75
+ code-graph-context init
76
+
77
+ # Add to Claude Code
78
+ claude mcp add code-graph-context code-graph-context
81
79
  ```
82
80
 
83
- 3. **Set up Neo4j using Docker:**
84
- ```bash
85
- docker-compose up -d
81
+ Then configure your OpenAI API key in `~/.config/claude/config.json`:
82
+ ```json
83
+ {
84
+ "mcpServers": {
85
+ "code-graph-context": {
86
+ "command": "code-graph-context",
87
+ "env": {
88
+ "OPENAI_API_KEY": "sk-your-key-here"
89
+ }
90
+ }
91
+ }
92
+ }
86
93
  ```
87
94
 
88
- This will start Neo4j with:
89
- - Web interface: http://localhost:7474
90
- - Bolt connection: bolt://localhost:7687
91
- - Username: `neo4j`, Password: `PASSWORD`
95
+ #### Option 2: From Source
92
96
 
93
- 4. **Configure environment variables:**
94
97
  ```bash
95
- cp .env.example .env
96
- # Edit .env with your configuration
98
+ # Clone and build
99
+ git clone https://github.com/drewdrewH/code-graph-context.git
100
+ cd code-graph-context
101
+ npm install
102
+ npm run build
103
+
104
+ # Set up Neo4j
105
+ code-graph-context init
106
+
107
+ # Add to Claude Code (use absolute path)
108
+ claude mcp add code-graph-context node /absolute/path/to/code-graph-context/dist/cli/cli.js
97
109
  ```
98
110
 
99
- 5. **Build the project:**
111
+ ### CLI Commands
112
+
113
+ The package includes a CLI for managing Neo4j:
114
+
100
115
  ```bash
101
- npm run build
116
+ code-graph-context init [options] # Set up Neo4j container
117
+ code-graph-context status # Check Docker/Neo4j status
118
+ code-graph-context stop # Stop Neo4j container
102
119
  ```
103
120
 
104
- 6. **Add to Claude Code:**
105
- ```bash
106
- claude mcp add code-graph-context node /absolute/path/to/code-graph-context/dist/mcp/mcp.server.js
121
+ **Init options:**
122
+ ```
123
+ -p, --port <port> Bolt port (default: 7687)
124
+ --http-port <port> Browser port (default: 7474)
125
+ --password <password> Neo4j password (default: PASSWORD)
126
+ -m, --memory <size> Heap memory (default: 2G)
127
+ -f, --force Recreate container
107
128
  ```
108
129
 
109
- #### Option 2: NPM Install (Global Package)
130
+ ### Alternative Neo4j Setup
110
131
 
111
- Best for: Easy setup and automatic updates
132
+ If you prefer not to use the CLI, you can set up Neo4j manually:
112
133
 
113
- 1. **Install the package globally:**
134
+ **Docker Compose:**
114
135
  ```bash
115
- npm install -g code-graph-context
136
+ docker-compose up -d
116
137
  ```
117
138
 
118
- 2. **Set up Neo4j** (choose one):
119
-
120
- **Option A: Docker (Recommended)**
139
+ **Docker Run:**
121
140
  ```bash
122
141
  docker run -d \
123
142
  --name code-graph-neo4j \
124
143
  -p 7474:7474 -p 7687:7687 \
125
144
  -e NEO4J_AUTH=neo4j/PASSWORD \
126
- -e NEO4J_PLUGINS='["apoc"]' \
145
+ -e 'NEO4J_PLUGINS=["apoc"]' \
127
146
  neo4j:5.23
128
147
  ```
129
148
 
130
- **Option B: Neo4j Desktop**
131
- - Download from [neo4j.com/download](https://neo4j.com/download/)
132
- - Install APOC plugin
133
- - Start database
134
-
135
- **Option C: Neo4j Aura (Cloud)**
136
- - Create free account at [neo4j.com/cloud/aura](https://neo4j.com/cloud/platform/aura-graph-database/)
137
- - Note your connection URI and credentials
138
-
139
- 3. **Add to Claude Code:**
140
- ```bash
141
- claude mcp add code-graph-context code-graph-context
142
- ```
143
-
144
- Then configure in your MCP config file (`~/.config/claude/config.json`):
145
- ```json
146
- {
147
- "mcpServers": {
148
- "code-graph-context": {
149
- "command": "code-graph-context",
150
- "env": {
151
- "OPENAI_API_KEY": "sk-your-key-here",
152
- "NEO4J_URI": "bolt://localhost:7687",
153
- "NEO4J_USER": "neo4j",
154
- "NEO4J_PASSWORD": "PASSWORD"
155
- }
156
- }
157
- }
158
- }
159
- ```
149
+ **Neo4j Desktop:** Download from [neo4j.com/download](https://neo4j.com/download/) and install APOC plugin.
160
150
 
161
- **Note:** The env vars can be configured for any Neo4j instance - local, Docker, cloud (Aura), or enterprise.
151
+ **Neo4j Aura (Cloud):** Create account at [neo4j.com/cloud/aura](https://neo4j.com/cloud/platform/aura-graph-database/) and configure connection URI in env vars.
162
152
 
163
153
  ### Verify Installation
164
154
 
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI Entry Point for Code Graph Context
4
+ *
5
+ * Handles CLI commands (init, status, stop) and delegates to MCP server
6
+ */
7
+ import { readFileSync } from 'fs';
8
+ import { dirname, join } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { Command } from 'commander';
11
+ import { NEO4J_CONFIG, createContainer, getContainerStatus, getFullStatus, isApocAvailable, isDockerInstalled, isDockerRunning, removeContainer, startContainer, stopContainer, waitForNeo4j, } from './neo4j-docker.js';
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+ // ANSI colors
15
+ const c = {
16
+ reset: '\x1b[0m',
17
+ bold: '\x1b[1m',
18
+ dim: '\x1b[2m',
19
+ green: '\x1b[32m',
20
+ red: '\x1b[31m',
21
+ yellow: '\x1b[33m',
22
+ blue: '\x1b[34m',
23
+ cyan: '\x1b[36m',
24
+ };
25
+ const sym = {
26
+ ok: `${c.green}✓${c.reset}`,
27
+ err: `${c.red}✗${c.reset}`,
28
+ warn: `${c.yellow}⚠${c.reset}`,
29
+ info: `${c.blue}ℹ${c.reset}`,
30
+ };
31
+ const log = (symbol, msg) => {
32
+ console.log(` ${symbol} ${msg}`);
33
+ };
34
+ const header = (text) => {
35
+ console.log(`\n${c.bold}${text}${c.reset}\n`);
36
+ };
37
+ /**
38
+ * Spinner for async operations
39
+ */
40
+ const spinner = (msg) => {
41
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
42
+ let i = 0;
43
+ const interval = setInterval(() => {
44
+ process.stdout.write(`\r ${c.blue}${frames[i]}${c.reset} ${msg}`);
45
+ i = (i + 1) % frames.length;
46
+ }, 80);
47
+ return {
48
+ stop: (ok, finalMsg) => {
49
+ clearInterval(interval);
50
+ process.stdout.write(`\r ${ok ? sym.ok : sym.err} ${finalMsg || msg}\n`);
51
+ },
52
+ };
53
+ };
54
+ /**
55
+ * Print config instructions
56
+ */
57
+ const printConfigInstructions = (password, boltPort) => {
58
+ console.log(`
59
+ ${c.bold}Next steps:${c.reset}
60
+
61
+ 1. Add to Claude Code:
62
+ ${c.dim}claude mcp add code-graph-context code-graph-context${c.reset}
63
+
64
+ 2. Configure in ${c.cyan}~/.config/claude/config.json${c.reset}:
65
+
66
+ ${c.dim}{
67
+ "mcpServers": {
68
+ "code-graph-context": {
69
+ "command": "code-graph-context",
70
+ "env": {
71
+ "OPENAI_API_KEY": "sk-..."${password !== NEO4J_CONFIG.defaultPassword
72
+ ? `,
73
+ "NEO4J_PASSWORD": "${password}"`
74
+ : ''}${boltPort !== NEO4J_CONFIG.boltPort
75
+ ? `,
76
+ "NEO4J_URI": "bolt://localhost:${boltPort}"`
77
+ : ''}
78
+ }
79
+ }
80
+ }
81
+ }${c.reset}
82
+
83
+ ${c.yellow}Get your OpenAI API key:${c.reset} https://platform.openai.com/api-keys
84
+
85
+ 3. Restart Claude Code
86
+ `);
87
+ };
88
+ /**
89
+ * Init command - set up Neo4j
90
+ */
91
+ const runInit = async (options) => {
92
+ const boltPort = options.port ? parseInt(options.port, 10) : NEO4J_CONFIG.boltPort;
93
+ const httpPort = options.httpPort ? parseInt(options.httpPort, 10) : NEO4J_CONFIG.httpPort;
94
+ const password = options.password || NEO4J_CONFIG.defaultPassword;
95
+ const memory = options.memory || '4G';
96
+ header('Code Graph Context Setup');
97
+ // Check Docker
98
+ if (!isDockerInstalled()) {
99
+ log(sym.err, 'Docker is not installed');
100
+ console.log(`\n Install Docker: ${c.cyan}https://docs.docker.com/get-docker/${c.reset}\n`);
101
+ process.exit(1);
102
+ }
103
+ log(sym.ok, 'Docker installed');
104
+ if (!isDockerRunning()) {
105
+ log(sym.err, 'Docker daemon is not running');
106
+ console.log(`\n Start Docker Desktop or run: ${c.dim}sudo systemctl start docker${c.reset}\n`);
107
+ process.exit(1);
108
+ }
109
+ log(sym.ok, 'Docker daemon running');
110
+ // Handle existing container
111
+ const status = getContainerStatus();
112
+ if (status === 'running' && !options.force) {
113
+ log(sym.ok, 'Neo4j container already running');
114
+ const apocOk = isApocAvailable(NEO4J_CONFIG.containerName, password);
115
+ log(apocOk ? sym.ok : sym.warn, apocOk ? 'APOC plugin available' : 'APOC plugin not detected');
116
+ console.log(`\n ${c.dim}Use --force to recreate the container${c.reset}`);
117
+ printConfigInstructions(password, boltPort);
118
+ return;
119
+ }
120
+ if (status !== 'not-found' && options.force) {
121
+ const s = spinner('Removing existing container...');
122
+ stopContainer();
123
+ removeContainer();
124
+ s.stop(true, 'Removed existing container');
125
+ }
126
+ if (status === 'stopped' && !options.force) {
127
+ const s = spinner('Starting existing container...');
128
+ const started = startContainer();
129
+ if (!started) {
130
+ s.stop(false, 'Failed to start container');
131
+ console.log(`\n Try: ${c.dim}code-graph-context init --force${c.reset}\n`);
132
+ process.exit(1);
133
+ }
134
+ s.stop(true, 'Container started');
135
+ }
136
+ else if (status === 'not-found' || options.force) {
137
+ const s = spinner('Creating Neo4j container...');
138
+ const created = createContainer({ httpPort, boltPort, password, memory });
139
+ if (!created) {
140
+ s.stop(false, 'Failed to create container');
141
+ console.log(`
142
+ Check if ports are in use:
143
+ ${c.dim}lsof -i :${httpPort}${c.reset}
144
+ ${c.dim}lsof -i :${boltPort}${c.reset}
145
+ `);
146
+ process.exit(1);
147
+ }
148
+ s.stop(true, 'Container created');
149
+ }
150
+ // Wait for Neo4j
151
+ const healthSpinner = spinner('Waiting for Neo4j to be ready (this may take a minute)...');
152
+ const ready = await waitForNeo4j(NEO4J_CONFIG.containerName, password);
153
+ healthSpinner.stop(ready, ready ? 'Neo4j is ready' : 'Neo4j failed to start');
154
+ if (!ready) {
155
+ console.log(`\n Check logs: ${c.dim}docker logs ${NEO4J_CONFIG.containerName}${c.reset}\n`);
156
+ process.exit(1);
157
+ }
158
+ // Check APOC
159
+ const apocOk = isApocAvailable(NEO4J_CONFIG.containerName, password);
160
+ log(apocOk ? sym.ok : sym.warn, apocOk ? 'APOC plugin verified' : 'APOC still loading (should be ready shortly)');
161
+ // Print connection info
162
+ console.log(`
163
+ ${c.bold}Neo4j is ready${c.reset}
164
+
165
+ Browser: ${c.cyan}http://localhost:${httpPort}${c.reset}
166
+ Bolt URI: ${c.cyan}bolt://localhost:${boltPort}${c.reset}
167
+ Credentials: ${c.dim}neo4j / ${password}${c.reset}`);
168
+ printConfigInstructions(password, boltPort);
169
+ };
170
+ /**
171
+ * Status command
172
+ */
173
+ const runStatus = () => {
174
+ header('Code Graph Context Status');
175
+ const status = getFullStatus();
176
+ log(status.dockerInstalled ? sym.ok : sym.err, `Docker installed: ${status.dockerInstalled ? 'yes' : 'no'}`);
177
+ if (!status.dockerInstalled) {
178
+ console.log(`\n Install: ${c.cyan}https://docs.docker.com/get-docker/${c.reset}\n`);
179
+ return;
180
+ }
181
+ log(status.dockerRunning ? sym.ok : sym.err, `Docker running: ${status.dockerRunning ? 'yes' : 'no'}`);
182
+ if (!status.dockerRunning) {
183
+ console.log(`\n Start Docker Desktop or: ${c.dim}sudo systemctl start docker${c.reset}\n`);
184
+ return;
185
+ }
186
+ const containerIcon = status.containerStatus === 'running' ? sym.ok : status.containerStatus === 'stopped' ? sym.warn : sym.err;
187
+ log(containerIcon, `Container: ${status.containerStatus}`);
188
+ if (status.containerStatus === 'running') {
189
+ log(status.neo4jReady ? sym.ok : sym.warn, `Neo4j responding: ${status.neo4jReady ? 'yes' : 'no'}`);
190
+ log(status.apocAvailable ? sym.ok : sym.warn, `APOC plugin: ${status.apocAvailable ? 'available' : 'not available'}`);
191
+ }
192
+ console.log('');
193
+ if (status.containerStatus !== 'running') {
194
+ console.log(` Run ${c.dim}code-graph-context init${c.reset} to start Neo4j\n`);
195
+ }
196
+ else if (!status.apocAvailable) {
197
+ console.log(` APOC may still be loading. Wait a moment and check again.\n`);
198
+ }
199
+ };
200
+ /**
201
+ * Stop command
202
+ */
203
+ const runStop = () => {
204
+ const status = getContainerStatus();
205
+ if (status === 'not-found') {
206
+ log(sym.info, 'No Neo4j container found');
207
+ return;
208
+ }
209
+ if (status === 'stopped') {
210
+ log(sym.info, 'Container already stopped');
211
+ return;
212
+ }
213
+ const s = spinner('Stopping Neo4j...');
214
+ const stopped = stopContainer();
215
+ s.stop(stopped, stopped ? 'Neo4j stopped' : 'Failed to stop container');
216
+ };
217
+ /**
218
+ * Start MCP server
219
+ */
220
+ const startMcpServer = async () => {
221
+ // The MCP server is in a sibling directory after build
222
+ // cli/cli.js -> mcp/mcp.server.js
223
+ const mcpPath = join(__dirname, '..', 'mcp', 'mcp.server.js');
224
+ await import(mcpPath);
225
+ };
226
+ /**
227
+ * Get package version
228
+ */
229
+ const getVersion = () => {
230
+ try {
231
+ // Go up from dist/cli to root
232
+ const pkgPath = join(__dirname, '..', '..', 'package.json');
233
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
234
+ return pkg.version;
235
+ }
236
+ catch {
237
+ return 'unknown';
238
+ }
239
+ };
240
+ // Build CLI
241
+ const program = new Command();
242
+ program.name('code-graph-context').description('MCP server for code graph analysis with Neo4j').version(getVersion());
243
+ program
244
+ .command('init')
245
+ .description('Set up Neo4j container and show configuration steps')
246
+ .option('-p, --port <port>', 'Neo4j Bolt port', '7687')
247
+ .option('--http-port <port>', 'Neo4j Browser port', '7474')
248
+ .option('--password <password>', 'Neo4j password', 'PASSWORD')
249
+ .option('-m, --memory <size>', 'Max heap memory (e.g., 2G, 4G)', '2G')
250
+ .option('-f, --force', 'Recreate container even if exists')
251
+ .action(runInit);
252
+ program.command('status').description('Check Neo4j and Docker status').action(runStatus);
253
+ program.command('stop').description('Stop the Neo4j container').action(runStop);
254
+ // Default action: start MCP server if no command given
255
+ const knownCommands = ['init', 'status', 'stop', 'help'];
256
+ const args = process.argv.slice(2);
257
+ const hasCommand = args.some((arg) => knownCommands.includes(arg) || arg.startsWith('-'));
258
+ if (args.length === 0 || !hasCommand) {
259
+ startMcpServer().catch((err) => {
260
+ console.error('Failed to start MCP server:', err);
261
+ process.exit(1);
262
+ });
263
+ }
264
+ else {
265
+ program.parse();
266
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Neo4j Docker Management
3
+ *
4
+ * Handles Docker container lifecycle for Neo4j
5
+ */
6
+ import { execSync } from 'child_process';
7
+ // Container configuration
8
+ export const NEO4J_CONFIG = {
9
+ containerName: 'code-graph-neo4j',
10
+ image: 'neo4j:5.23',
11
+ httpPort: 7474,
12
+ boltPort: 7687,
13
+ defaultPassword: 'PASSWORD',
14
+ defaultUser: 'neo4j',
15
+ healthCheckTimeoutMs: 120000,
16
+ healthCheckIntervalMs: 2000,
17
+ };
18
+ /**
19
+ * Execute a command and return stdout, or null if failed
20
+ */
21
+ const exec = (command) => {
22
+ try {
23
+ return execSync(command, {
24
+ encoding: 'utf-8',
25
+ stdio: ['pipe', 'pipe', 'pipe'],
26
+ }).trim();
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ };
32
+ /**
33
+ * Check if Docker CLI is available
34
+ */
35
+ export const isDockerInstalled = () => exec('docker --version') !== null;
36
+ /**
37
+ * Check if Docker daemon is running
38
+ */
39
+ export const isDockerRunning = () => exec('docker info') !== null;
40
+ /**
41
+ * Get container status
42
+ */
43
+ export const getContainerStatus = (containerName = NEO4J_CONFIG.containerName) => {
44
+ const result = exec(`docker inspect --format='{{.State.Running}}' ${containerName} 2>/dev/null`);
45
+ if (result === null)
46
+ return 'not-found';
47
+ return result === 'true' ? 'running' : 'stopped';
48
+ };
49
+ /**
50
+ * Start an existing stopped container
51
+ */
52
+ export const startContainer = (containerName = NEO4J_CONFIG.containerName) => exec(`docker start ${containerName}`) !== null;
53
+ /**
54
+ * Stop a running container
55
+ */
56
+ export const stopContainer = (containerName = NEO4J_CONFIG.containerName) => exec(`docker stop ${containerName}`) !== null;
57
+ /**
58
+ * Remove a container
59
+ */
60
+ export const removeContainer = (containerName = NEO4J_CONFIG.containerName) => exec(`docker rm ${containerName}`) !== null;
61
+ /**
62
+ * Create and start a new Neo4j container
63
+ */
64
+ export const createContainer = (options = {}) => {
65
+ const { containerName = NEO4J_CONFIG.containerName, httpPort = NEO4J_CONFIG.httpPort, boltPort = NEO4J_CONFIG.boltPort, password = NEO4J_CONFIG.defaultPassword, memory = '2G', } = options;
66
+ const cmd = [
67
+ 'docker run -d',
68
+ `--name ${containerName}`,
69
+ `--restart unless-stopped`,
70
+ `-p ${httpPort}:7474`,
71
+ `-p ${boltPort}:7687`,
72
+ `-e NEO4J_AUTH=neo4j/${password}`,
73
+ `-e 'NEO4J_PLUGINS=["apoc"]'`,
74
+ `-e NEO4J_dbms_security_procedures_unrestricted=apoc.*`,
75
+ `-e NEO4J_server_memory_heap_initial__size=1G`,
76
+ `-e NEO4J_server_memory_heap_max__size=${memory}`,
77
+ `-e NEO4J_server_memory_pagecache_size=2G`,
78
+ NEO4J_CONFIG.image,
79
+ ].join(' ');
80
+ return exec(cmd) !== null;
81
+ };
82
+ /**
83
+ * Check if Neo4j is accepting connections
84
+ */
85
+ export const isNeo4jReady = (containerName = NEO4J_CONFIG.containerName, password = NEO4J_CONFIG.defaultPassword) => {
86
+ const result = exec(`docker exec ${containerName} cypher-shell -u neo4j -p ${password} "RETURN 1" 2>/dev/null`);
87
+ return result !== null;
88
+ };
89
+ /**
90
+ * Check if APOC plugin is available
91
+ */
92
+ export const isApocAvailable = (containerName = NEO4J_CONFIG.containerName, password = NEO4J_CONFIG.defaultPassword) => {
93
+ const result = exec(`docker exec ${containerName} cypher-shell -u neo4j -p ${password} "CALL apoc.help('apoc') YIELD name RETURN count(name)" 2>/dev/null`);
94
+ return result !== null && !result.includes('error');
95
+ };
96
+ /**
97
+ * Wait for Neo4j to be ready
98
+ */
99
+ export const waitForNeo4j = async (containerName = NEO4J_CONFIG.containerName, password = NEO4J_CONFIG.defaultPassword, timeoutMs = NEO4J_CONFIG.healthCheckTimeoutMs) => {
100
+ const startTime = Date.now();
101
+ while (Date.now() - startTime < timeoutMs) {
102
+ if (isNeo4jReady(containerName, password)) {
103
+ return true;
104
+ }
105
+ await new Promise((resolve) => setTimeout(resolve, NEO4J_CONFIG.healthCheckIntervalMs));
106
+ }
107
+ return false;
108
+ };
109
+ /**
110
+ * Ensure Neo4j is running - start if needed
111
+ */
112
+ export const ensureNeo4jRunning = async (options = {}) => {
113
+ const containerName = options.containerName ?? NEO4J_CONFIG.containerName;
114
+ const status = getContainerStatus(containerName);
115
+ if (status === 'running') {
116
+ return { success: true, action: 'already-running' };
117
+ }
118
+ if (!isDockerInstalled()) {
119
+ return { success: false, action: 'failed', error: 'Docker not installed' };
120
+ }
121
+ if (!isDockerRunning()) {
122
+ return { success: false, action: 'failed', error: 'Docker daemon not running' };
123
+ }
124
+ // Start existing container
125
+ if (status === 'stopped') {
126
+ if (startContainer(containerName)) {
127
+ const ready = await waitForNeo4j(containerName, options.password);
128
+ return ready
129
+ ? { success: true, action: 'started' }
130
+ : { success: false, action: 'failed', error: 'Container started but Neo4j not responding' };
131
+ }
132
+ return { success: false, action: 'failed', error: 'Failed to start existing container' };
133
+ }
134
+ // Create new container
135
+ if (createContainer(options)) {
136
+ const ready = await waitForNeo4j(containerName, options.password);
137
+ return ready
138
+ ? { success: true, action: 'created' }
139
+ : { success: false, action: 'failed', error: 'Container created but Neo4j not responding' };
140
+ }
141
+ return { success: false, action: 'failed', error: 'Failed to create container' };
142
+ };
143
+ /**
144
+ * Get full status for diagnostics
145
+ */
146
+ export const getFullStatus = () => {
147
+ const dockerInstalled = isDockerInstalled();
148
+ const dockerRunning = dockerInstalled && isDockerRunning();
149
+ const containerStatus = dockerRunning ? getContainerStatus() : 'not-found';
150
+ const neo4jReady = containerStatus === 'running' && isNeo4jReady();
151
+ const apocAvailable = neo4jReady && isApocAvailable();
152
+ return {
153
+ dockerInstalled,
154
+ dockerRunning,
155
+ containerStatus,
156
+ neo4jReady,
157
+ apocAvailable,
158
+ };
159
+ };
@@ -525,13 +525,11 @@ export const FAIRSQUARE_FRAMEWORK_SCHEMA = {
525
525
  {
526
526
  type: 'function',
527
527
  pattern: (parsedNode) => {
528
- const node = parsedNode.sourceNode;
529
- if (!node || !Node.isVariableDeclaration(node))
530
- return false;
531
- const name = node.getName();
532
- const typeNode = node.getTypeNode();
528
+ // Use pre-extracted properties (works after AST cleanup in streaming/chunking)
529
+ const name = parsedNode.properties.name;
530
+ const typeAnnotation = parsedNode.properties.typeAnnotation;
533
531
  // Check if variable name ends with "Routes" AND has type ModuleRoute[]
534
- return !!name.endsWith('Routes') && !!typeNode?.getText().includes('ModuleRoute');
532
+ return !!name?.endsWith('Routes') && !!typeAnnotation?.includes('ModuleRoute');
535
533
  },
536
534
  confidence: 1.0,
537
535
  priority: 10,
@@ -696,14 +694,8 @@ export const FAIRSQUARE_FRAMEWORK_SCHEMA = {
696
694
  if (matchingRoutes.length === 0)
697
695
  return false;
698
696
  // CRITICAL FIX: Verify the method belongs to the correct controller
699
- // Find the parent class of this method by checking the AST node
700
- const targetNode = parsedTargetNode.sourceNode;
701
- if (!targetNode || !Node.isMethodDeclaration(targetNode))
702
- return false;
703
- const parentClass = targetNode.getParent();
704
- if (!parentClass || !Node.isClassDeclaration(parentClass))
705
- return false;
706
- const parentClassName = parentClass.getName();
697
+ // Use pre-extracted parentClassName property (works after AST cleanup in streaming/chunking)
698
+ const parentClassName = parsedTargetNode.properties.parentClassName;
707
699
  if (!parentClassName)
708
700
  return false;
709
701
  // Check if any matching route's controller name matches the parent class
@@ -434,7 +434,7 @@ export const NESTJS_FRAMEWORK_SCHEMA = {
434
434
  },
435
435
  {
436
436
  type: 'function',
437
- pattern: (node) => node.getName()?.endsWith('Service'),
437
+ pattern: (parsedNode) => parsedNode.sourceNode?.getName()?.endsWith('Service'),
438
438
  confidence: 0.7,
439
439
  priority: 7,
440
440
  },
@@ -516,7 +516,10 @@ export const NESTJS_FRAMEWORK_SCHEMA = {
516
516
  detectionPatterns: [
517
517
  {
518
518
  type: 'function',
519
- pattern: (node) => {
519
+ pattern: (parsedNode) => {
520
+ const node = parsedNode.sourceNode;
521
+ if (!node)
522
+ return false;
520
523
  const decorators = node.getDecorators?.() ?? [];
521
524
  const messageDecorators = ['MessagePattern', 'EventPattern'];
522
525
  return decorators.some((d) => messageDecorators.includes(d.getName()));
@@ -567,7 +570,10 @@ export const NESTJS_FRAMEWORK_SCHEMA = {
567
570
  detectionPatterns: [
568
571
  {
569
572
  type: 'function',
570
- pattern: (node) => {
573
+ pattern: (parsedNode) => {
574
+ const node = parsedNode.sourceNode;
575
+ if (!node)
576
+ return false;
571
577
  const decorators = node.getDecorators?.() ?? [];
572
578
  const httpDecorators = ['Get', 'Post', 'Put', 'Delete', 'Patch', 'Head', 'Options'];
573
579
  return decorators.some((d) => httpDecorators.includes(d.getName()));
@@ -480,6 +480,12 @@ export const CORE_TYPESCRIPT_SCHEMA = {
480
480
  extraction: { method: 'static', defaultValue: false }, // We'll set this manually
481
481
  neo4j: { indexed: true, unique: false, required: true },
482
482
  },
483
+ {
484
+ name: 'typeAnnotation',
485
+ type: 'string',
486
+ extraction: { method: 'ast', source: 'getTypeNode', transform: 'getText' },
487
+ neo4j: { indexed: false, unique: false, required: false },
488
+ },
483
489
  ],
484
490
  relationships: [],
485
491
  children: {},
@@ -277,16 +277,24 @@ Provide ONLY the JSON response with no additional text, markdown formatting, or
277
277
  frameworkHint = '\nFRAMEWORK DETECTED: React/functional codebase. Use Function nodes for components.';
278
278
  }
279
279
  return `
280
- ACTUAL GRAPH SCHEMA (use these exact labels):
280
+ === VALID NODE LABELS (use ONLY these after the colon) ===
281
+ ${nodeTypes}
281
282
 
282
- Node Types: ${nodeTypes}
283
- Relationship Types: ${relTypes}
284
- Semantic Types: ${semTypes}
283
+ === VALID RELATIONSHIP TYPES ===
284
+ ${relTypes}
285
+
286
+ === SEMANTIC TYPES (these are PROPERTY values, NOT labels) ===
287
+ ${semTypes}
288
+ Query semantic types via property: WHERE n.semanticType = 'TypeName'
285
289
  ${frameworkHint}
286
- CRITICAL: Use ONLY these node labels. Do NOT invent labels like :DbService, :UserService, etc.
287
- For queries about specific classes/services, use: (n:Class {name: 'ClassName'})
288
- For inheritance: (child:Class)-[:EXTENDS]->(parent:Class {name: 'ParentName'})
289
- For decorator-based queries: Use semanticType property with values from the discovered semantic types above.
290
+
291
+ === CRITICAL RULES ===
292
+ 1. Use ONLY the labels listed above after the colon (:Label)
293
+ 2. Semantic types are PROPERTY values, NOT labels
294
+ 3. Class/service names are PROPERTY values, NOT labels
295
+ 4. WRONG: (n:MyService), (n:MyController) - names as labels
296
+ 5. CORRECT: (n:Service {name: 'MyService'}), (n:Controller {name: 'MyController'})
297
+ 6. CORRECT: (n:Class) WHERE n.semanticType = 'Service'
290
298
  `.trim();
291
299
  }
292
300
  catch (error) {
@@ -507,7 +515,7 @@ Remember to include WHERE n.projectId = $projectId for all node patterns.
507
515
  }
508
516
  /**
509
517
  * Load valid labels dynamically from the schema file.
510
- * Returns all keys from rawSchema which represent actual Neo4j labels.
518
+ * Returns all keys from rawSchema AND discoveredSchema.nodeTypes which represent actual Neo4j labels.
511
519
  */
512
520
  loadValidLabelsFromSchema() {
513
521
  // Fallback to core TypeScript labels if schema not available
@@ -535,12 +543,21 @@ Remember to include WHERE n.projectId = $projectId for all node patterns.
535
543
  try {
536
544
  const content = fs.readFileSync(this.schemaPath, 'utf-8');
537
545
  const schema = JSON.parse(content);
538
- if (!schema.rawSchema?.records?.[0]?._fields?.[0]) {
539
- return coreLabels;
546
+ const allLabels = new Set(coreLabels);
547
+ // Extract labels from rawSchema keys
548
+ if (schema.rawSchema?.records?.[0]?._fields?.[0]) {
549
+ const schemaLabels = Object.keys(schema.rawSchema.records[0]._fields[0]);
550
+ schemaLabels.forEach((label) => allLabels.add(label));
551
+ }
552
+ // Also extract labels from discoveredSchema.nodeTypes (includes framework labels)
553
+ if (schema.discoveredSchema?.nodeTypes) {
554
+ for (const nodeType of schema.discoveredSchema.nodeTypes) {
555
+ if (nodeType.label) {
556
+ allLabels.add(nodeType.label);
557
+ }
558
+ }
540
559
  }
541
- // Extract all keys from rawSchema - these are the valid labels
542
- const schemaLabels = Object.keys(schema.rawSchema.records[0]._fields[0]);
543
- return new Set([...coreLabels, ...schemaLabels]);
560
+ return allLabels;
544
561
  }
545
562
  catch {
546
563
  return coreLabels;
@@ -542,6 +542,11 @@ export class TypeScriptParser {
542
542
  return astNode.getName();
543
543
  }
544
544
  break;
545
+ case CoreNodeType.VARIABLE_DECLARATION:
546
+ if (Node.isVariableDeclaration(astNode)) {
547
+ return astNode.getName();
548
+ }
549
+ break;
545
550
  default:
546
551
  return astNode.getKindName();
547
552
  }
@@ -552,13 +557,18 @@ export class TypeScriptParser {
552
557
  return 'Unknown';
553
558
  }
554
559
  extractProperty(astNode, propDef) {
555
- const { method, source, defaultValue } = propDef.extraction;
560
+ const { method, source, defaultValue, transform } = propDef.extraction;
556
561
  try {
557
562
  switch (method) {
558
563
  case 'ast':
559
564
  if (typeof source === 'string') {
560
565
  const fn = astNode[source];
561
- return typeof fn === 'function' ? fn.call(astNode) : defaultValue;
566
+ let result = typeof fn === 'function' ? fn.call(astNode) : defaultValue;
567
+ // Apply transform if specified (e.g., 'getText' on a returned node)
568
+ if (result && transform && typeof result[transform] === 'function') {
569
+ result = result[transform]();
570
+ }
571
+ return result ?? defaultValue;
562
572
  }
563
573
  return defaultValue;
564
574
  case 'function':
@@ -1284,11 +1294,9 @@ export class TypeScriptParser {
1284
1294
  }
1285
1295
  case 'function':
1286
1296
  if (typeof pattern.pattern === 'function') {
1287
- // Pass the AST sourceNode to pattern functions, not the ParsedNode wrapper
1288
- const astNode = node.sourceNode;
1289
- if (!astNode)
1290
- return false;
1291
- return pattern.pattern(astNode);
1297
+ // Pass the ParsedNode to pattern functions
1298
+ // Patterns should use pre-extracted properties for cross-chunk compatibility
1299
+ return pattern.pattern(node);
1292
1300
  }
1293
1301
  return false;
1294
1302
  case 'classname':
@@ -4,14 +4,67 @@
4
4
  */
5
5
  import fs from 'fs/promises';
6
6
  import { join } from 'path';
7
+ import { ensureNeo4jRunning, isDockerInstalled, isDockerRunning, } from '../cli/neo4j-docker.js';
7
8
  import { Neo4jService, QUERIES } from '../storage/neo4j/neo4j.service.js';
8
9
  import { FILE_PATHS, LOG_CONFIG } from './constants.js';
9
10
  import { initializeNaturalLanguageService } from './tools/natural-language-to-cypher.tool.js';
10
11
  import { debugLog } from './utils.js';
12
+ /**
13
+ * Log startup warnings for missing configuration
14
+ */
15
+ const checkConfiguration = async () => {
16
+ if (!process.env.OPENAI_API_KEY) {
17
+ console.error(JSON.stringify({
18
+ level: 'warn',
19
+ message: '[code-graph-context] OPENAI_API_KEY not set. Semantic search and NL queries unavailable.',
20
+ }));
21
+ await debugLog('Configuration warning', { warning: 'OPENAI_API_KEY not set' });
22
+ }
23
+ };
24
+ /**
25
+ * Ensure Neo4j is running - auto-start if Docker available, fail if not
26
+ */
27
+ const ensureNeo4j = async () => {
28
+ // Check if Docker is available
29
+ if (!isDockerInstalled()) {
30
+ const msg = 'Docker not installed. Install Docker or run: code-graph-context init';
31
+ console.error(JSON.stringify({ level: 'error', message: `[code-graph-context] ${msg}` }));
32
+ throw new Error(msg);
33
+ }
34
+ if (!isDockerRunning()) {
35
+ const msg = 'Docker not running. Start Docker or run: code-graph-context init';
36
+ console.error(JSON.stringify({ level: 'error', message: `[code-graph-context] ${msg}` }));
37
+ throw new Error(msg);
38
+ }
39
+ const result = await ensureNeo4jRunning();
40
+ if (!result.success) {
41
+ const msg = `Neo4j failed to start: ${result.error}. Run: code-graph-context init`;
42
+ console.error(JSON.stringify({ level: 'error', message: `[code-graph-context] ${msg}` }));
43
+ throw new Error(msg);
44
+ }
45
+ if (result.action === 'created') {
46
+ console.error(JSON.stringify({
47
+ level: 'info',
48
+ message: '[code-graph-context] Neo4j container created and started',
49
+ }));
50
+ }
51
+ else if (result.action === 'started') {
52
+ console.error(JSON.stringify({
53
+ level: 'info',
54
+ message: '[code-graph-context] Neo4j container started',
55
+ }));
56
+ }
57
+ await debugLog('Neo4j ready', result);
58
+ };
11
59
  /**
12
60
  * Initialize all external services required by the MCP server
13
61
  */
14
62
  export const initializeServices = async () => {
63
+ // Check for missing configuration (non-fatal warnings)
64
+ await checkConfiguration();
65
+ // Ensure Neo4j is running (fatal if not)
66
+ await ensureNeo4j();
67
+ // Initialize services
15
68
  await Promise.all([initializeNeo4jSchema(), initializeNaturalLanguageService()]);
16
69
  };
17
70
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-graph-context",
3
- "version": "2.3.0",
3
+ "version": "2.4.1",
4
4
  "description": "MCP server that builds code graphs to provide rich context to LLMs",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/drewdrewH/code-graph-context#readme",
@@ -30,7 +30,7 @@
30
30
  "license": "MIT",
31
31
  "main": "dist/mcp/mcp.server.js",
32
32
  "bin": {
33
- "code-graph-context": "dist/mcp/mcp.server.js"
33
+ "code-graph-context": "dist/cli/cli.js"
34
34
  },
35
35
  "files": [
36
36
  "dist/**/*",