Skip to content

Commit cef9c10

Browse files
authored
cypher - add warnings and info for server config, update changelog (#138)
* add warnings and info for server config, update changelog * formatting check * formatting check
1 parent da8b436 commit cef9c10

File tree

10 files changed

+750
-192
lines changed

10 files changed

+750
-192
lines changed

servers/mcp-neo4j-cypher/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* Update Neo4j Driver syntax to use `driver.execute_query(...)`. This cleans the driver code.
77

88
### Added
9+
* Add clear warnings for config declaration via cli and env variables
910

1011
## v0.3.0
1112

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import argparse
22
import asyncio
3-
import os
43

54
from . import server
5+
from .utils import process_config
66

77

88
def main():
@@ -12,26 +12,19 @@ def main():
1212
parser.add_argument("--username", default=None, help="Neo4j username")
1313
parser.add_argument("--password", default=None, help="Neo4j password")
1414
parser.add_argument("--database", default=None, help="Neo4j database name")
15-
parser.add_argument("--transport", default=None, help="Transport type (stdio, sse, http)")
15+
parser.add_argument(
16+
"--transport", default=None, help="Transport type (stdio, sse, http)"
17+
)
1618
parser.add_argument("--namespace", default=None, help="Tool namespace")
17-
parser.add_argument("--server-path", default=None, help="HTTP path (default: /mcp/)")
19+
parser.add_argument(
20+
"--server-path", default=None, help="HTTP path (default: /mcp/)"
21+
)
1822
parser.add_argument("--server-host", default=None, help="Server host")
1923
parser.add_argument("--server-port", default=None, help="Server port")
2024

2125
args = parser.parse_args()
22-
asyncio.run(
23-
server.main(
24-
args.db_url or os.getenv("NEO4J_URL") or os.getenv("NEO4J_URI", "bolt://localhost:7687"),
25-
args.username or os.getenv("NEO4J_USERNAME", "neo4j"),
26-
args.password or os.getenv("NEO4J_PASSWORD", "password"),
27-
args.database or os.getenv("NEO4J_DATABASE", "neo4j"),
28-
args.transport or os.getenv("NEO4J_TRANSPORT", "stdio"),
29-
args.namespace or os.getenv("NEO4J_NAMESPACE", ""),
30-
args.server_host or os.getenv("NEO4J_MCP_SERVER_HOST", "127.0.0.1"),
31-
args.server_port or int(os.getenv("NEO4J_MCP_SERVER_PORT", "8000")),
32-
args.server_path or os.getenv("NEO4J_MCP_SERVER_PATH", "/mcp/"),
33-
)
34-
)
26+
config = process_config(args)
27+
asyncio.run(server.main(**config))
3528

3629

3730
__all__ = ["main", "server"]

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

Lines changed: 87 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,16 @@
44
from typing import Any, Literal
55

66
from fastmcp.exceptions import ToolError
7-
from fastmcp.tools.tool import ToolResult, TextContent
87
from fastmcp.server import FastMCP
8+
from fastmcp.tools.tool import TextContent, ToolResult
99
from mcp.types import ToolAnnotations
10-
from neo4j import (
11-
AsyncDriver,
12-
AsyncGraphDatabase,
13-
RoutingControl
14-
)
10+
from neo4j import AsyncDriver, AsyncGraphDatabase, RoutingControl
1511
from neo4j.exceptions import ClientError, Neo4jError
1612
from pydantic import Field
1713

1814
logger = logging.getLogger("mcp_neo4j_cypher")
1915

16+
2017
def _format_namespace(namespace: str) -> str:
2118
if namespace:
2219
if namespace.endswith("-"):
@@ -26,6 +23,7 @@ def _format_namespace(namespace: str) -> str:
2623
else:
2724
return ""
2825

26+
2927
def _is_write_query(query: str) -> bool:
3028
"""Check if the query is a write query."""
3129
return (
@@ -34,19 +32,25 @@ def _is_write_query(query: str) -> bool:
3432
)
3533

3634

37-
def create_mcp_server(neo4j_driver: AsyncDriver, database: str = "neo4j", namespace: str = "") -> FastMCP:
38-
mcp: FastMCP = FastMCP("mcp-neo4j-cypher", dependencies=["neo4j", "pydantic"], stateless_http=True)
35+
def create_mcp_server(
36+
neo4j_driver: AsyncDriver, database: str = "neo4j", namespace: str = ""
37+
) -> FastMCP:
38+
mcp: FastMCP = FastMCP(
39+
"mcp-neo4j-cypher", dependencies=["neo4j", "pydantic"], stateless_http=True
40+
)
3941

4042
namespace_prefix = _format_namespace(namespace)
4143

42-
@mcp.tool(name=namespace_prefix+"get_neo4j_schema",
43-
annotations=ToolAnnotations(title="Get Neo4j Schema",
44-
readOnlyHint=True,
45-
destructiveHint=False,
46-
idempotentHint=True,
47-
openWorldHint=True
48-
)
49-
)
44+
@mcp.tool(
45+
name=namespace_prefix + "get_neo4j_schema",
46+
annotations=ToolAnnotations(
47+
title="Get Neo4j Schema",
48+
readOnlyHint=True,
49+
destructiveHint=False,
50+
idempotentHint=True,
51+
openWorldHint=True,
52+
),
53+
)
5054
async def get_neo4j_schema() -> list[ToolResult]:
5155
"""
5256
List all nodes, their attributes and their relationships to other nodes in the neo4j database.
@@ -61,10 +65,7 @@ def clean_schema(schema: dict) -> dict:
6165
cleaned = {}
6266

6367
for key, entry in schema.items():
64-
65-
new_entry = {
66-
"type": entry["type"]
67-
}
68+
new_entry = {"type": entry["type"]}
6869
if "count" in entry:
6970
new_entry["count"] = entry["count"]
7071

@@ -119,42 +120,47 @@ def clean_schema(schema: dict) -> dict:
119120

120121
return cleaned
121122

122-
123123
try:
124+
results_json_str = await neo4j_driver.execute_query(
125+
get_schema_query,
126+
routing_control=RoutingControl.READ,
127+
database_=database,
128+
result_transformer_=lambda r: r.data(),
129+
)
124130

125-
results_json_str = await neo4j_driver.execute_query(get_schema_query,
126-
routing_control=RoutingControl.READ,
127-
database_=database,
128-
result_transformer_=lambda r: r.data())
129-
130131
logger.debug(f"Read query returned {len(results_json_str)} rows")
131132

132-
schema_clean = clean_schema(results_json_str[0].get('value'))
133+
schema_clean = clean_schema(results_json_str[0].get("value"))
133134

134135
schema_clean_str = json.dumps(schema_clean, default=str)
135136

136137
return ToolResult(content=[TextContent(type="text", text=schema_clean_str)])
137-
138+
138139
except ClientError as e:
139140
if "Neo.ClientError.Procedure.ProcedureNotFound" in str(e):
140-
raise ToolError("Neo4j Client Error: This instance of Neo4j does not have the APOC plugin installed. Please install and enable the APOC plugin to use the `get_neo4j_schema` tool.")
141+
raise ToolError(
142+
"Neo4j Client Error: This instance of Neo4j does not have the APOC plugin installed. Please install and enable the APOC plugin to use the `get_neo4j_schema` tool."
143+
)
141144
else:
142145
raise ToolError(f"Neo4j Client Error: {e}")
143-
146+
144147
except Neo4jError as e:
145148
raise ToolError(f"Neo4j Error: {e}")
146-
149+
147150
except Exception as e:
148151
logger.error(f"Error retrieving Neo4j database schema: {e}")
149152
raise ToolError(f"Unexpected Error: {e}")
150153

151-
@mcp.tool(name=namespace_prefix+"read_neo4j_cypher",
152-
annotations=ToolAnnotations(title="Read Neo4j Cypher",
153-
readOnlyHint=True,
154-
destructiveHint=False,
155-
idempotentHint=True,
156-
openWorldHint=True
157-
))
154+
@mcp.tool(
155+
name=namespace_prefix + "read_neo4j_cypher",
156+
annotations=ToolAnnotations(
157+
title="Read Neo4j Cypher",
158+
readOnlyHint=True,
159+
destructiveHint=False,
160+
idempotentHint=True,
161+
openWorldHint=True,
162+
),
163+
)
158164
async def read_neo4j_cypher(
159165
query: str = Field(..., description="The Cypher query to execute."),
160166
params: dict[str, Any] = Field(
@@ -167,33 +173,38 @@ async def read_neo4j_cypher(
167173
raise ValueError("Only MATCH queries are allowed for read-query")
168174

169175
try:
170-
results = await neo4j_driver.execute_query(query,
171-
parameters_=params,
172-
routing_control=RoutingControl.READ,
173-
database_=database,
174-
result_transformer_=lambda r: r.data())
175-
176+
results = await neo4j_driver.execute_query(
177+
query,
178+
parameters_=params,
179+
routing_control=RoutingControl.READ,
180+
database_=database,
181+
result_transformer_=lambda r: r.data(),
182+
)
183+
176184
results_json_str = json.dumps(results, default=str)
177185

178186
logger.debug(f"Read query returned {len(results_json_str)} rows")
179187

180188
return ToolResult(content=[TextContent(type="text", text=results_json_str)])
181-
189+
182190
except Neo4jError as e:
183191
logger.error(f"Neo4j Error executing read query: {e}\n{query}\n{params}")
184192
raise ToolError(f"Neo4j Error: {e}\n{query}\n{params}")
185-
193+
186194
except Exception as e:
187195
logger.error(f"Error executing read query: {e}\n{query}\n{params}")
188196
raise ToolError(f"Error: {e}\n{query}\n{params}")
189197

190-
@mcp.tool(name=namespace_prefix+"write_neo4j_cypher",
191-
annotations=ToolAnnotations(title="Write Neo4j Cypher",
192-
readOnlyHint=False,
193-
destructiveHint=True,
194-
idempotentHint=False,
195-
openWorldHint=True
196-
))
198+
@mcp.tool(
199+
name=namespace_prefix + "write_neo4j_cypher",
200+
annotations=ToolAnnotations(
201+
title="Write Neo4j Cypher",
202+
readOnlyHint=False,
203+
destructiveHint=True,
204+
idempotentHint=False,
205+
openWorldHint=True,
206+
),
207+
)
197208
async def write_neo4j_cypher(
198209
query: str = Field(..., description="The Cypher query to execute."),
199210
params: dict[str, Any] = Field(
@@ -206,22 +217,25 @@ async def write_neo4j_cypher(
206217
raise ValueError("Only write queries are allowed for write-query")
207218

208219
try:
209-
_, summary, _ = await neo4j_driver.execute_query(query,
210-
parameters_=params,
211-
routing_control=RoutingControl.WRITE,
212-
database_=database,
213-
)
214-
220+
_, summary, _ = await neo4j_driver.execute_query(
221+
query,
222+
parameters_=params,
223+
routing_control=RoutingControl.WRITE,
224+
database_=database,
225+
)
226+
215227
counters_json_str = json.dumps(summary.counters.__dict__, default=str)
216228

217229
logger.debug(f"Write query affected {counters_json_str}")
218230

219-
return ToolResult(content=[TextContent(type="text", text=counters_json_str)])
231+
return ToolResult(
232+
content=[TextContent(type="text", text=counters_json_str)]
233+
)
220234

221235
except Neo4jError as e:
222236
logger.error(f"Neo4j Error executing write query: {e}\n{query}\n{params}")
223237
raise ToolError(f"Neo4j Error: {e}\n{query}\n{params}")
224-
238+
225239
except Exception as e:
226240
logger.error(f"Error executing write query: {e}\n{query}\n{params}")
227241
raise ToolError(f"Error: {e}\n{query}\n{params}")
@@ -255,17 +269,25 @@ async def main(
255269
# Run the server with the specified transport
256270
match transport:
257271
case "http":
258-
logger.info(f"Running Neo4j Cypher MCP Server with HTTP transport on {host}:{port}...")
272+
logger.info(
273+
f"Running Neo4j Cypher MCP Server with HTTP transport on {host}:{port}..."
274+
)
259275
await mcp.run_http_async(host=host, port=port, path=path)
260276
case "stdio":
261277
logger.info("Running Neo4j Cypher MCP Server with stdio transport...")
262278
await mcp.run_stdio_async()
263279
case "sse":
264-
logger.info(f"Running Neo4j Cypher MCP Server with SSE transport on {host}:{port}...")
280+
logger.info(
281+
f"Running Neo4j Cypher MCP Server with SSE transport on {host}:{port}..."
282+
)
265283
await mcp.run_sse_async(host=host, port=port, path=path)
266284
case _:
267-
logger.error(f"Invalid transport: {transport} | Must be either 'stdio', 'sse', or 'http'")
268-
raise ValueError(f"Invalid transport: {transport} | Must be either 'stdio', 'sse', or 'http'")
285+
logger.error(
286+
f"Invalid transport: {transport} | Must be either 'stdio', 'sse', or 'http'"
287+
)
288+
raise ValueError(
289+
f"Invalid transport: {transport} | Must be either 'stdio', 'sse', or 'http'"
290+
)
269291

270292

271293
if __name__ == "__main__":

0 commit comments

Comments
 (0)