Neo4j MCP Knowledge Graph Server
by jfelipenc
README.md
# Neo4j MCP Knowledge Graph Server
A containerized FastAPI + MCP (Model Context Protocol) server that lets LLM agents inject structured knowledge (Entities and Relationships) into a Neo4j graph database with APOC-based safe Cypher execution.
## Features
- **Dual API**: REST endpoints + MCP SSE transport for LLM agent integration
- **Safe Cypher**: APOC procedures prevent Cypher injection with dynamic labels/types
- **Full-text search**: Cross-label search via Neo4j full-text index
- **Schema flexibility**: Agents can invent node labels and relationship types (tagged with `is_generated: true`)
- **API key auth**: All traffic protected by `X-API-Key` header
- **Docker Compose**: One-command deployment with Neo4j 5 + APOC
## Prerequisites
- Docker Desktop (with Docker Compose)
- Git
- (Optional) Python 3.10+ for local development
## Quick Start
### 1. Clone the repository
```bash
git clone https://github.com/jfelipenc/neo4j-mcp-knowledge-graph.git
cd neo4j-mcp-knowledge-graph
```
### 2. Configure environment
```bash
cp .env.example .env
```
Edit `.env` and set your values:
```env
# Required: Change this to a secure random string
API_KEY=your-secure-api-key-here
# Neo4j connection (defaults work with docker-compose)
NEO4J_URI=bolt://neo4j:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=testpassword123
```
**Important**: Change `API_KEY` to a secure random string. Generate one with:
```bash
# Linux/macOS
openssl rand -hex 32
# Windows PowerShell
[guid]::NewGuid().ToString("N")
```
### 3. Change the Neo4j password (optional but recommended)
Edit `docker-compose.yml` and update both services:
```yaml
neo4j:
environment:
- NEO4J_AUTH=neo4j/your-new-password-here
...
healthcheck:
test: ["CMD", "cypher-shell", "-u", "neo4j", "-p", "your-new-password-here", "RETURN 1"]
api:
environment:
- NEO4J_PASSWORD=your-new-password-here
```
Also update `.env`:
```env
NEO4J_PASSWORD=your-new-password-here
```
### 4. Start the stack
```bash
docker-compose up --build
```
Wait for Neo4j to be healthy (the API service depends on it):
```
neo4j-mcp-knowledge-graph-neo4j-1 | Started.
neo4j-mcp-knowledge-graph-api-1 | INFO: Uvicorn running on http://0.0.0.0:8000
```
### 5. Verify it's running
```bash
# Health check (no auth required)
curl http://localhost:8000/health
# Test auth (replace with your API key)
curl -X POST http://localhost:8000/api/knowledge/entities \
-H "X-API-Key: your-secure-api-key-here" \
-H "Content-Type: application/json" \
-d '{"entities": [{"name": "Alice", "label": "Person"}]}'
```
## API Documentation
### Authentication
All endpoints (except `/health`) require the `X-API-Key` header.
### REST Endpoints
#### Add Entities
```bash
POST /api/knowledge/entities
Content-Type: application/json
X-API-Key: your-api-key
{
"entities": [
{
"name": "Alice",
"label": "Person",
"properties": {"age": 30, "city": "NYC"},
"is_generated": false
},
{
"name": "GraphDB",
"label": "Technology",
"properties": {"vendor": "Neo4j"},
"is_generated": true
}
]
}
```
Response:
```json
{"count": 2}
```
#### Add Relations
```bash
POST /api/knowledge/relations
Content-Type: application/json
X-API-Key: your-api-key
{
"relations": [
{
"source_name": "Alice",
"source_label": "Person",
"target_name": "GraphDB",
"target_label": "Technology",
"relation_type": "USES",
"properties": {"since": "2024"},
"is_generated": false
}
]
}
```
Response:
```json
{"count": 1}
```
#### Search Graph
```bash
GET /api/knowledge/search?q=Alice&limit=10
X-API-Key: your-api-key
```
Response:
```json
{
"nodes": [
{
"name": "Alice",
"label": "Person",
"properties": {"age": 30, "city": "NYC"}
}
],
"edges": [
{
"source": "Alice",
"target": "GraphDB",
"type": "USES",
"properties": {"since": "2024"}
}
]
}
```
**Search tips**:
- Use `*` for prefix matching: `Alice*` finds `Alice`, `AliceSmith`
- Search is case-insensitive on the full-text index
- Limit defaults to 10, max 100
### MCP (Model Context Protocol)
The server exposes MCP tools via SSE (Server-Sent Events) transport.
#### Connect to SSE
```bash
GET /mcp/sse
X-API-Key: your-api-key
```
This opens an SSE stream. The MCP client will receive an `endpoint` event with the URL to POST messages to.
#### MCP Tools
| Tool | Description | Parameters |
|------|-------------|------------|
| `add_entities` | Add nodes to the graph | `entities: list[Entity]` |
| `add_relations` | Add relationships between nodes | `relations: list[Relation]` |
| `search_graph` | Search nodes by name, return subgraph | `query: str, limit: int = 10` |
**Entity schema:**
```json
{
"name": "string (required)",
"label": "string (required)",
"properties": {"key": "value"},
"is_generated": "boolean (default: false)"
}
```
**Relation schema:**
```json
{
"source_name": "string (required)",
"source_label": "string (required)",
"target_name": "string (required)",
"target_label": "string (required)",
"relation_type": "string (required)",
"properties": {"key": "value"},
"is_generated": "boolean (default: false)"
}
```
## Local Development
### Setup
```bash
# Create virtual environment
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # Linux/macOS
# Install dependencies
pip install -r requirements.txt
```
### Run tests
```bash
# Unit tests (fast, no Docker required)
pytest tests/ -m "not integration" -v
# Integration tests (requires Docker, spins up Neo4j container)
pytest tests/ -m integration -v
# All tests
pytest tests/ -v
```
### Run locally (without Docker)
```bash
# Start Neo4j separately (e.g., via Docker)
docker run -p 7474:7474 -p 7687:7687 \
-e NEO4J_AUTH=neo4j/testpassword123 \
-e NEO4J_PLUGINS='["apoc"]' \
-e NEO4J_dbms_security_procedures_unrestricted=apoc.* \
neo4j:5
# Set environment variables
$env:API_KEY="dev-api-key"
$env:NEO4J_URI="bolt://localhost:7687"
$env:NEO4J_USER="neo4j"
$env:NEO4J_PASSWORD="testpassword123"
# Run the app
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
```
## Project Structure
```
/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI app, auth, REST endpoints, MCP SSE wiring
│ ├── mcp_server.py # MCP tools (add_entities, add_relations, search_graph)
│ ├── database.py # Neo4j driver, APOC merges, full-text search
│ └── schemas.py # Pydantic models (Entity, Relation)
├── tests/
│ ├── test_schemas.py
│ ├── test_database.py
│ ├── test_mcp_server.py
│ ├── test_main.py
│ └── test_integration.py
├── docker-compose.yml # Neo4j 5 + APOC + API service
├── Dockerfile # Python 3.11 app container
├── requirements.txt
├── .env.example # Environment template
└── README.md
```
## Security Notes
- **API key**: Always change the default `API_KEY` in production
- **Neo4j password**: Change the default `testpassword123` in production
- **Cypher injection**: APOC procedures prevent injection via dynamic labels/types
- **Timing attacks**: API key comparison uses `secrets.compare_digest`
- **Auth coverage**: All routes except `/health` require authentication
## Troubleshooting
### Neo4j container won't start
```bash
# Check logs
docker-compose logs neo4j
# Common fix: remove stale volume and restart
docker-compose down -v
docker-compose up --build
```
### API can't connect to Neo4j
- Verify Neo4j is healthy: `docker-compose ps`
- Check the password matches in both `docker-compose.yml` and `.env`
- Ensure `NEO4J_URI` uses the service name (`bolt://neo4j:7687`) not localhost
### Full-text search returns no results
- Neo4j full-text indexes are eventually consistent — wait a moment after writes
- Use prefix wildcards: `Alice*` instead of `Alice` for partial matching
- Verify the index exists: `SHOW INDEXES` in Neo4j Browser (http://localhost:7474)
### Port conflicts
If ports 8000, 7474, or 7687 are in use, edit `docker-compose.yml`:
```yaml
api:
ports:
- "8001:8000" # Change 8001 to an available port
```
## License
MIT
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues