Skip to content

Commit 9b1332f

Browse files
authored
mcp-neo4j-cypher clean up (#18)
* update readme, remove IT, move changelog to root * Update CHANGELOG.txt * make changelog markdown file * add integration tests, update readme * Update pr-mcp-neo4j-cypher.yml * add ruff to dev deps, developing PR workflow * formatting, fix wf
1 parent 6760480 commit 9b1332f

File tree

12 files changed

+307
-141
lines changed

12 files changed

+307
-141
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: MCP Neo4j Cypher Tests
2+
3+
on:
4+
push:
5+
branches: [ main, master ]
6+
paths:
7+
- 'servers/mcp-neo4j-cypher/**'
8+
pull_request:
9+
branches: [ main, master ]
10+
paths:
11+
- 'servers/mcp-neo4j-cypher/**'
12+
workflow_dispatch: # Allows manual triggering of the workflow
13+
14+
jobs:
15+
test:
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- uses: actions/checkout@v3
20+
21+
- name: Set up Python 3.12
22+
uses: actions/setup-python@v4
23+
with:
24+
python-version: '3.12'
25+
26+
- name: Install UV
27+
run: |
28+
curl -LsSf https://astral.sh/uv/install.sh | sh
29+
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
30+
31+
- name: Install dependencies
32+
run: |
33+
cd servers/mcp-neo4j-cypher
34+
uv venv
35+
uv pip install -e ".[dev]"
36+
37+
- name: Check format and linting
38+
run: |
39+
cd servers/mcp-neo4j-cypher
40+
uv run ruff check --select I . --fix
41+
uv run ruff check --fix .
42+
uv run ruff format .
43+
44+
- name: Run tests
45+
run: |
46+
cd servers/mcp-neo4j-cypher
47+
./test.sh

servers/mcp-neo4j-cypher/src/mcp_neo4j_cypher/CHANGELOG.txt renamed to servers/mcp-neo4j-cypher/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
* Refactor mcp-neo4j-cypher to use the FastMCP class
88
* Implement Neo4j async driver
99
* Tool responses now return JSON serialized results
10+
* Update README with new config options
11+
* Update integration tests
1012

1113
### Added
1214

1315
* Add support for environment variables
16+
* Add Github workflow to test and format mcp-neo4j-cypher
1417

1518

1619
## v0.1.1

servers/mcp-neo4j-cypher/README.md

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,44 +15,40 @@ The server offers these core tools:
1515
- Execute Cypher read queries to read data from the database
1616
- Input:
1717
- `query` (string): The Cypher query to execute
18-
- Returns: Query results as array of objects
18+
- `params` (dictionary, optional): Parameters to pass to the Cypher query
19+
- Returns: Query results as JSON serialized array of objects
1920

2021
- `write-neo4j-cypher`
2122
- Execute updating Cypher queries
2223
- Input:
2324
- `query` (string): The Cypher update query
24-
- Returns: A result summary counter with `{ nodes_updated: number, relationships_created: number, ... }`
25+
- `params` (dictionary, optional): Parameters to pass to the Cypher query
26+
- Returns: A JSON serialized result summary counter with `{ nodes_updated: number, relationships_created: number, ... }`
2527

2628
#### 🕸️ Schema Tools
2729
- `get-neo4j-schema`
2830
- Get a list of all nodes types in the graph database, their attributes with name, type and relationships to other node types
2931
- No input required
30-
- Returns: List of node label with two dictionaries one for attributes and one for relationships
32+
- Returns: JSON serialized list of node labels with two dictionaries: one for attributes and one for relationships
3133

3234
## 🔧 Usage with Claude Desktop
3335

3436
### 💾 Released Package
3537

3638
Can be found on PyPi https://pypi.org/project/mcp-neo4j-cypher/
3739

38-
Add the server to your `claude_desktop_config.json` with configuration of:
39-
40-
* db-url
41-
* username
42-
* password
43-
44-
45-
Alternatively, you can set environment variables:
40+
Add the server to your `claude_desktop_config.json` with configuration through environment variables:
4641

4742
```json
4843
"mcpServers": {
4944
"neo4j-aura": {
5045
"command": "uvx",
5146
"args": [ "mcp-neo4j-cypher==0.1.2" ],
5247
"env": {
53-
"NEO4J_URL": "bolt://localhost:7687",
48+
"NEO4J_URI": "bolt://localhost:7687",
5449
"NEO4J_USERNAME": "neo4j",
55-
"NEO4J_PASSWORD": "<your-password>"
50+
"NEO4J_PASSWORD": "<your-password>",
51+
"NEO4J_DATABASE": "neo4j"
5652
}
5753
}
5854
}
@@ -66,17 +62,17 @@ Here is an example connection for the movie database with Movie, Person (Actor,
6662
"movies-neo4j": {
6763
"command": "uvx",
6864
"args": ["mcp-neo4j-cypher==0.1.2"],
69-
"env": {
70-
"NEO4J_URL": "neo4j+s://demo.neo4jlabs.com",
71-
"NEO4J_USERNAME": "recommendations",
72-
"NEO4J_PASSWORD": "recommendations"
73-
}
65+
"env": {
66+
"NEO4J_URI": "neo4j+s://demo.neo4jlabs.com",
67+
"NEO4J_USERNAME": "recommendations",
68+
"NEO4J_PASSWORD": "recommendations"
69+
}
7470
}
7571
}
7672
}
7773
```
7874

79-
Syntax with `--db-url`, `--username` and `--password` was supported but will be removed in future versions:
75+
Syntax with `--db-url`, `--username` and `--password` command line arguments is still supported but environment variables are preferred:
8076

8177
<details>
8278
<summary>Legacy Syntax</summary>
@@ -124,7 +120,7 @@ Here is an example connection for the movie database with Movie, Person (Actor,
124120
"args": [
125121
"run",
126122
"--rm",
127-
"-e", "NEO4J_URL=bolt://host.docker.internal:7687",
123+
"-e", "NEO4J_URI=bolt://host.docker.internal:7687",
128124
"-e", "NEO4J_USERNAME=neo4j",
129125
"-e", "NEO4J_PASSWORD=<your-password>",
130126
"mcp/neo4j-cypher:0.1.2"
@@ -164,6 +160,17 @@ source .venv/bin/activate # On Unix/macOS
164160
uv pip install -e ".[dev]"
165161
```
166162

163+
3. Run Integration Tests
164+
165+
**CLOSE ANY LOCAL NEO4J DATABASES BEFORE RUNNING TESTS**
166+
* Tests will deploy a local docker container containing the test Neo4j instance.
167+
* However if a Neo4j database is running locally, then the test driver may connect here instead.
168+
* **This will result in you local Neo4j database having its contents erased.**
169+
170+
```bash
171+
./tests.sh
172+
```
173+
167174
### 🔧 Development Configuration
168175

169176
```json
@@ -175,7 +182,7 @@ uv pip install -e ".[dev]"
175182
"--directory", "parent_of_servers_repo/servers/mcp-neo4j-cypher/src",
176183
"run", "mcp-neo4j-cypher"],
177184
"env": {
178-
"NEO4J_URL": "bolt://localhost",
185+
"NEO4J_URI": "bolt://localhost",
179186
"NEO4J_USERNAME": "neo4j",
180187
"NEO4J_PASSWORD": "<your-password>"
181188
}
@@ -192,7 +199,7 @@ Build and run the Docker container:
192199
docker build -t mcp/neo4j-cypher:latest .
193200

194201
# Run the container
195-
docker run -e NEO4J_URL="bolt://host.docker.internal:7687" \
202+
docker run -e NEO4J_URI="bolt://host.docker.internal:7687" \
196203
-e NEO4J_USERNAME="neo4j" \
197204
-e NEO4J_PASSWORD="your-password" \
198205
mcp/neo4j-cypher:latest

servers/mcp-neo4j-cypher/pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ requires = ["hatchling"]
1515
build-backend = "hatchling.build"
1616

1717
[tool.uv]
18-
dev-dependencies = ["pyright>=1.1.389", "pytest>=7.0.0", "pytest-asyncio>=0.20.3"]
18+
dev-dependencies = [
19+
"pyright>=1.1.389",
20+
"pytest>=7.0.0",
21+
"pytest-asyncio>=0.20.3",
22+
"ruff>=0.11.5",
23+
]
1924

2025
[project.scripts]
2126
mcp-neo4j-cypher = "mcp_neo4j_cypher:main"

servers/mcp-neo4j-cypher/src/mcp_neo4j_cypher/__init__.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
from . import server
2-
import asyncio
31
import argparse
2+
import asyncio
43
import os
54

5+
from . import server
6+
67

78
def main():
89
"""Main entry point for the package."""
@@ -22,8 +23,5 @@ def main():
2223
)
2324
)
2425

25-
# asyncio.run(server.main())
26-
2726

28-
# Optionally expose other important items at package level
2927
__all__ = ["main", "server"]

servers/mcp-neo4j-cypher/src/mcp_neo4j_cypher/server.py

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
1+
import json
12
import logging
3+
import re
4+
import sys
5+
import time
6+
from typing import Any, Optional
7+
28
import mcp.types as types
39
from mcp.server.fastmcp import FastMCP
4-
from pydantic import Field
5-
from typing import Any
610
from neo4j import (
11+
AsyncDriver,
712
AsyncGraphDatabase,
813
AsyncResult,
914
AsyncTransaction,
1015
GraphDatabase,
1116
)
1217
from neo4j.exceptions import DatabaseError
13-
import time
14-
import re
15-
import os
16-
from typing import Optional
17-
import sys
18-
import json
18+
from pydantic import Field
19+
20+
logger = logging.getLogger("mcp_neo4j_cypher")
1921

2022

2123
def healthcheck(db_url: str, username: str, password: str, database: str) -> None:
@@ -78,26 +80,9 @@ def _is_write_query(query: str) -> bool:
7880
)
7981

8082

81-
def main(
82-
db_url: str,
83-
username: str,
84-
password: str,
85-
database: str,
86-
) -> None:
87-
logger = logging.getLogger("mcp_neo4j_cypher")
88-
logger.info("Starting MCP neo4j Server")
89-
90-
neo4j_driver = AsyncGraphDatabase.driver(
91-
db_url,
92-
auth=(
93-
username,
94-
password,
95-
),
96-
)
97-
83+
def create_mcp_server(neo4j_driver: AsyncDriver, database: str = "neo4j") -> FastMCP:
9884
mcp: FastMCP = FastMCP("mcp-neo4j-cypher", dependencies=["neo4j", "pydantic"])
9985

100-
@mcp.tool()
10186
async def get_neo4j_schema() -> list[types.TextContent]:
10287
"""List all node, their attributes and their relationships to other nodes in the neo4j database"""
10388

@@ -111,9 +96,7 @@ async def get_neo4j_schema() -> list[types.TextContent]:
11196
"""
11297

11398
try:
114-
async with neo4j_driver.session(
115-
database=os.getenv("NEO4J_DATABASE", "neo4j")
116-
) as session:
99+
async with neo4j_driver.session(database=database) as session:
117100
results_json_str = await session.execute_read(
118101
_read, get_schema_query, dict()
119102
)
@@ -126,7 +109,6 @@ async def get_neo4j_schema() -> list[types.TextContent]:
126109
logger.error(f"Database error retrieving schema: {e}")
127110
return [types.TextContent(type="text", text=f"Error: {e}")]
128111

129-
@mcp.tool()
130112
async def read_neo4j_cypher(
131113
query: str = Field(..., description="The Cypher query to execute."),
132114
params: Optional[dict[str, Any]] = Field(
@@ -139,9 +121,7 @@ async def read_neo4j_cypher(
139121
raise ValueError("Only MATCH queries are allowed for read-query")
140122

141123
try:
142-
async with neo4j_driver.session(
143-
database=os.getenv("NEO4J_DATABASE", "neo4j")
144-
) as session:
124+
async with neo4j_driver.session(database=database) as session:
145125
results_json_str = await session.execute_read(_read, query, params)
146126

147127
logger.debug(f"Read query returned {len(results_json_str)} rows")
@@ -154,7 +134,6 @@ async def read_neo4j_cypher(
154134
types.TextContent(type="text", text=f"Error: {e}\n{query}\n{params}")
155135
]
156136

157-
@mcp.tool()
158137
async def write_neo4j_cypher(
159138
query: str = Field(..., description="The Cypher query to execute."),
160139
params: Optional[dict[str, Any]] = Field(
@@ -167,11 +146,11 @@ async def write_neo4j_cypher(
167146
raise ValueError("Only write queries are allowed for write-query")
168147

169148
try:
170-
async with neo4j_driver.session(
171-
database=os.getenv("NEO4J_DATABASE", "neo4j")
172-
) as session:
149+
async with neo4j_driver.session(database=database) as session:
173150
raw_results = await session.execute_write(_write, query, params)
174-
counters_json_str = json.dumps(raw_results._summary.counters.__dict__, default=str)
151+
counters_json_str = json.dumps(
152+
raw_results._summary.counters.__dict__, default=str
153+
)
175154

176155
logger.debug(f"Write query affected {counters_json_str}")
177156

@@ -183,6 +162,31 @@ async def write_neo4j_cypher(
183162
types.TextContent(type="text", text=f"Error: {e}\n{query}\n{params}")
184163
]
185164

165+
mcp.add_tool(get_neo4j_schema)
166+
mcp.add_tool(read_neo4j_cypher)
167+
mcp.add_tool(write_neo4j_cypher)
168+
169+
return mcp
170+
171+
172+
def main(
173+
db_url: str,
174+
username: str,
175+
password: str,
176+
database: str,
177+
) -> None:
178+
logger.info("Starting MCP neo4j Server")
179+
180+
neo4j_driver = AsyncGraphDatabase.driver(
181+
db_url,
182+
auth=(
183+
username,
184+
password,
185+
),
186+
)
187+
188+
mcp = create_mcp_server(neo4j_driver, database)
189+
186190
healthcheck(db_url, username, password, database)
187191

188192
mcp.run(transport="stdio")

servers/mcp-neo4j-cypher/test.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
uv run --env-file ../.env pytest tests/test_neo4j_cypher_integration.py
1+
docker compose -f tests/integration/docker-compose.yml up -d
2+
uv run pytest tests/integration -s
3+
docker compose -f tests/integration/docker-compose.yml stop

0 commit comments

Comments
 (0)