Skip to main content
Glama
jfelipenc

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