code-graph-context 2.3.0 → 2.4.0
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 +54 -64
- package/dist/cli/cli.js +266 -0
- package/dist/cli/neo4j-docker.js +159 -0
- package/dist/mcp/service-init.js +53 -0
- package/package.json +2 -2
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:
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
```
|
|
71
|
+
# Install globally
|
|
72
|
+
npm install -g code-graph-context
|
|
77
73
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
```
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
111
|
+
### CLI Commands
|
|
112
|
+
|
|
113
|
+
The package includes a CLI for managing Neo4j:
|
|
114
|
+
|
|
100
115
|
```bash
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
```
|
|
106
|
-
|
|
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
|
-
|
|
130
|
+
### Alternative Neo4j Setup
|
|
110
131
|
|
|
111
|
-
|
|
132
|
+
If you prefer not to use the CLI, you can set up Neo4j manually:
|
|
112
133
|
|
|
113
|
-
|
|
134
|
+
**Docker Compose:**
|
|
114
135
|
```bash
|
|
115
|
-
|
|
136
|
+
docker-compose up -d
|
|
116
137
|
```
|
|
117
138
|
|
|
118
|
-
|
|
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=
|
|
145
|
+
-e 'NEO4J_PLUGINS=["apoc"]' \
|
|
127
146
|
neo4j:5.23
|
|
128
147
|
```
|
|
129
148
|
|
|
130
|
-
**
|
|
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
|
-
**
|
|
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
|
|
package/dist/cli/cli.js
ADDED
|
@@ -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
|
+
};
|
package/dist/mcp/service-init.js
CHANGED
|
@@ -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
|
+
"version": "2.4.0",
|
|
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/
|
|
33
|
+
"code-graph-context": "dist/cli/cli.js"
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"dist/**/*",
|