Skip to content

Commit c6cf090

Browse files
authored
Merge branch 'main' into 75-schema-incl-rel-properties
2 parents 754d369 + 00b5d61 commit c6cf090

File tree

6 files changed

+230
-16
lines changed

6 files changed

+230
-16
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: MCP Neo4j Cypher Generate and Upload DXT File
2+
3+
on:
4+
push:
5+
tags:
6+
- mcp-neo4j-cypher-v*
7+
workflow_dispatch: # Allows manual triggering of the workflow
8+
inputs:
9+
release_tag:
10+
description: 'Release tag to add .dxt file to (e.g., mcp-neo4j-cypher-v1.0.0)'
11+
required: true
12+
type: string
13+
14+
permissions:
15+
contents: write
16+
17+
jobs:
18+
generate-dxt:
19+
runs-on: ubuntu-latest
20+
21+
steps:
22+
- name: Determine tag name
23+
id: tag
24+
run: |
25+
if [ "${{ github.event_name }}" = "push" ]; then
26+
TAG_NAME="${{ github.ref_name }}"
27+
echo "Tag from push event: $TAG_NAME"
28+
else
29+
TAG_NAME="${{ github.event.inputs.release_tag }}"
30+
echo "Tag from manual input: $TAG_NAME"
31+
fi
32+
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
33+
34+
- name: Checkout code at specific tag
35+
uses: actions/checkout@v4
36+
with:
37+
ref: ${{ steps.tag.outputs.tag_name }}
38+
39+
- name: Setup Python
40+
uses: actions/setup-python@v5
41+
with:
42+
python-version-file: "servers/mcp-neo4j-cypher/pyproject.toml"
43+
44+
- name: Install uv
45+
uses: astral-sh/setup-uv@v5
46+
47+
- name: Setup Node.js for dxt
48+
uses: actions/setup-node@v4
49+
with:
50+
node-version: '18'
51+
52+
- name: Install Anthropic dxt
53+
run: npm install -g @anthropic-ai/dxt
54+
55+
- name: Setup Python environment and dependencies with uv
56+
run: |
57+
cd servers/mcp-neo4j-cypher/
58+
uv sync
59+
# Install the package in development mode
60+
uv pip install -e .
61+
62+
- name: Generate .dxt file
63+
run: |
64+
cd servers/mcp-neo4j-cypher/
65+
66+
echo "Running dxt pack with uv..."
67+
uv run dxt pack .
68+
69+
- name: Upload .dxt file to release
70+
env:
71+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
72+
run: |
73+
gh release upload "${{ steps.tag.outputs.tag_name }}" \
74+
servers/mcp-neo4j-cypher/*.dxt \
75+
--clobber

servers/mcp-neo4j-cypher/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
### Fixed
44

55
### Changed
6+
* Update error handling in read and write tools
67

78
### Added
9+
* Add .dxt file for Cypher MCP server
10+
* Add .dxt file generation to Cypher MCP Publish GitHub action
11+
* Add error indicator to tool results in the `CallToolResult` object
812

913
## v0.2.4
1014

servers/mcp-neo4j-cypher/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ This is useful when you need to connect to multiple Neo4j databases or instances
3939

4040
## 🔧 Usage with Claude Desktop
4141

42+
### Using DXT
43+
Download the latest `.dxt` file from the [releases page](https://github.com/neo4j-contrib/mcp-neo4j/releases/latest) and install it with your MCP client.
44+
45+
Or use this direct link:
46+
[Download mcp-neo4j-cypher.dxt](https://github.com/neo4j-contrib/mcp-neo4j/releases/latest/download/mcp-neo4j-cypher.dxt)
47+
4248
### 💾 Released Package
4349

4450
Can be found on PyPi https://pypi.org/project/mcp-neo4j-cypher/
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
{
2+
"dxt_version": "0.1",
3+
"name": "mcp-neo4j-cypher",
4+
"display_name": "Neo4j Cypher MCP Server",
5+
"version": "0.2.4",
6+
"description": "Execute read and write Cypher queries on your Neo4j database.",
7+
"long_description": "A Model Context Protocol (MCP) server that provides tools for interacting with Neo4j graph databases using Cypher queries. Supports both read and write operations with proper validation and error handling.",
8+
"author": {
9+
"name": "Alexander Gilmore"
10+
},
11+
"keywords": ["neo4j", "cypher", "graph", "database", "mcp", "ai", "llm"],
12+
"categories": ["database", "graph", "query"],
13+
"repository": {
14+
"type": "git",
15+
"url": "https://github.com/neo4j-contrib/mcp-neo4j/tree/main/servers/mcp-neo4j-cypher"
16+
},
17+
"documentation": "https://github.com/neo4j-contrib/mcp-neo4j/blob/main/servers/mcp-neo4j-cypher/README.md",
18+
"support": "https://github.com/neo4j-contrib/mcp-neo4j/issues",
19+
"server": {
20+
"type": "python",
21+
"entry_point": "src/mcp_neo4j_cypher/__init__.py",
22+
"mcp_config": {
23+
"command": "uvx",
24+
"args": ["mcp-neo4j-cypher"],
25+
"env": {
26+
"NEO4J_URI": "${user_config.neo4j_uri}",
27+
"NEO4J_USERNAME": "${user_config.neo4j_username}",
28+
"NEO4J_PASSWORD": "${user_config.neo4j_password}",
29+
"NEO4J_DATABASE": "${user_config.neo4j_database}",
30+
"NEO4J_TRANSPORT": "${user_config.transport}",
31+
"NEO4J_NAMESPACE": "${user_config.neo4j_namespace}",
32+
"NEO4J_MCP_SERVER_HOST": "${user_config.mcp_server_host}",
33+
"NEO4J_MCP_SERVER_PORT": "${user_config.mcp_server_port}"
34+
}
35+
}
36+
},
37+
"tools": [
38+
{
39+
"name": "get_neo4j_schema",
40+
"description": "Retrieve the schema of the Neo4j database, including node labels, properties, and relationships"
41+
},
42+
{
43+
"name": "read_neo4j_cypher",
44+
"description": "Execute read-only Cypher queries (MATCH, RETURN, etc.) on the Neo4j database"
45+
},
46+
{
47+
"name": "write_neo4j_cypher",
48+
"description": "Execute write Cypher queries (CREATE, MERGE, SET, DELETE, etc.) on the Neo4j database"
49+
}
50+
],
51+
"prompts": [],
52+
"tools_generated": false,
53+
"license": "MIT",
54+
"user_config": {
55+
"neo4j_username": {
56+
"type": "string",
57+
"title": "Neo4j Username",
58+
"description": "The username for logging into Neo4j",
59+
"default": "neo4j",
60+
"required": true,
61+
"sensitive": true
62+
},
63+
"neo4j_password": {
64+
"type": "string",
65+
"title": "Neo4j Password",
66+
"description": "The password for logging into Neo4j",
67+
"default": "password",
68+
"required": true,
69+
"sensitive": true
70+
},
71+
"neo4j_database": {
72+
"type": "string",
73+
"title": "Neo4j Database",
74+
"description": "The database to use in Neo4j, defaults to neo4j",
75+
"default": "neo4j",
76+
"required": false,
77+
"sensitive": true
78+
},
79+
"neo4j_uri": {
80+
"type": "string",
81+
"title": "Neo4j URI",
82+
"description": "The URI for connecting to Neo4j",
83+
"default": "bolt://localhost:7687",
84+
"required": true,
85+
"sensitive": true
86+
},
87+
"neo4j_namespace": {
88+
"type": "string",
89+
"title": "Namespace",
90+
"description": "An optional namespace for the MCP server tools",
91+
"default": "",
92+
"required": false,
93+
"sensitive": false
94+
},
95+
"transport": {
96+
"type": "string",
97+
"title": "Transport",
98+
"description": "The MCP transport, defaults to stdio",
99+
"default": "stdio",
100+
"required": false,
101+
"sensitive": false
102+
},
103+
"mcp_server_host": {
104+
"type": "string",
105+
"title": "MCP Server Host",
106+
"description": "The host for the MCP server, if not using stdio. Defaults to 127.0.0.1",
107+
"default": "127.0.0.1",
108+
"required": false,
109+
"sensitive": false
110+
},
111+
"mcp_server_port": {
112+
"type": "number",
113+
"title": "MCP Server Port",
114+
"description": "The port for the MCP server, if not using stdio. Defaults to 8000",
115+
"default": 8000,
116+
"required": false,
117+
"sensitive": false
118+
}
119+
}
120+
}

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

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,14 @@ def clean_schema(schema: dict) -> dict:
137137
schema_clean = clean_schema(schema)
138138
schema_clean_str = json.dumps(schema_clean)
139139

140-
return [types.TextContent(type="text", text=schema_clean_str)]
140+
return types.CallToolResult(content=[types.TextContent(type="text", text=schema_clean_str)])
141141

142142
except Exception as e:
143143
logger.error(f"Database error retrieving schema: {e}")
144-
return [types.TextContent(type="text", text=f"Error: {e}")]
144+
return types.CallToolResult(
145+
isError=True,
146+
content=[types.TextContent(type="text", text=f"Error: {e}")]
147+
)
145148

146149
async def read_neo4j_cypher(
147150
query: str = Field(..., description="The Cypher query to execute."),
@@ -151,22 +154,25 @@ async def read_neo4j_cypher(
151154
) -> list[types.TextContent]:
152155
"""Execute a read Cypher query on the neo4j database."""
153156

154-
if _is_write_query(query):
155-
raise ValueError("Only MATCH queries are allowed for read-query")
156-
157157
try:
158+
if _is_write_query(query):
159+
raise ValueError("Only MATCH queries are allowed for read-query")
160+
158161
async with neo4j_driver.session(database=database) as session:
159162
results_json_str = await session.execute_read(_read, query, params)
160163

161164
logger.debug(f"Read query returned {len(results_json_str)} rows")
162165

163-
return [types.TextContent(type="text", text=results_json_str)]
166+
return types.CallToolResult(content=[types.TextContent(type="text", text=results_json_str)])
164167

165168
except Exception as e:
166169
logger.error(f"Database error executing query: {e}\n{query}\n{params}")
167-
return [
170+
return types.CallToolResult(
171+
isError=True,
172+
content=[
168173
types.TextContent(type="text", text=f"Error: {e}\n{query}\n{params}")
169174
]
175+
)
170176

171177
async def write_neo4j_cypher(
172178
query: str = Field(..., description="The Cypher query to execute."),
@@ -176,10 +182,10 @@ async def write_neo4j_cypher(
176182
) -> list[types.TextContent]:
177183
"""Execute a write Cypher query on the neo4j database."""
178184

179-
if not _is_write_query(query):
180-
raise ValueError("Only write queries are allowed for write-query")
181-
182185
try:
186+
if not _is_write_query(query):
187+
raise ValueError("Only write queries are allowed for write-query")
188+
183189
async with neo4j_driver.session(database=database) as session:
184190
raw_results = await session.execute_write(_write, query, params)
185191
counters_json_str = json.dumps(
@@ -188,13 +194,16 @@ async def write_neo4j_cypher(
188194

189195
logger.debug(f"Write query affected {counters_json_str}")
190196

191-
return [types.TextContent(type="text", text=counters_json_str)]
197+
return types.CallToolResult(content=[types.TextContent(type="text", text=counters_json_str)])
192198

193199
except Exception as e:
194200
logger.error(f"Database error executing query: {e}\n{query}\n{params}")
195-
return [
201+
return types.CallToolResult(
202+
isError=True,
203+
content=[
196204
types.TextContent(type="text", text=f"Error: {e}\n{query}\n{params}")
197205
]
206+
)
198207

199208
namespace_prefix = _format_namespace(namespace)
200209

servers/mcp-neo4j-cypher/tests/integration/test_server_IT.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
async def test_get_neo4j_schema(mcp_server: FastMCP, init_data: Any):
1010
response = await mcp_server.call_tool("get_neo4j_schema", dict())
1111

12-
schema = json.loads(response[0].text)[0]
12+
temp_parsed = json.loads(response[0].text)['content'][0]['text']
13+
schema = json.loads(temp_parsed)[0]
1314

1415
# Verify the schema result
1516
assert "label" in schema
@@ -22,8 +23,7 @@ async def test_write_neo4j_cypher(mcp_server: FastMCP):
2223
# Execute a Cypher query to create a node
2324
query = "CREATE (n:Test {name: 'test', age: 123}) RETURN n.name"
2425
response = await mcp_server.call_tool("write_neo4j_cypher", dict(query=query))
25-
26-
result = json.loads(response[0].text)
26+
result = json.loads(json.loads(response[0].text)['content'][0]['text'])
2727
# Verify the node creation
2828
assert len(result) == 4
2929
assert result["nodes_created"] == 1
@@ -43,7 +43,7 @@ async def test_read_neo4j_cypher(mcp_server: FastMCP, init_data: Any):
4343
"""
4444

4545
response = await mcp_server.call_tool("read_neo4j_cypher", dict(query=query))
46-
result = json.loads(response[0].text)
46+
result = json.loads(json.loads(response[0].text)['content'][0]['text'])
4747
# # Verify the query result
4848
assert len(result) == 2
4949
assert result[0]["person"] == "Alice"

0 commit comments

Comments
 (0)