44from typing import Any , Literal
55
66from fastmcp .exceptions import ToolError
7- from fastmcp .tools .tool import ToolResult , TextContent
87from fastmcp .server import FastMCP
8+ from fastmcp .tools .tool import TextContent , ToolResult
99from mcp .types import ToolAnnotations
10- from neo4j import (
11- AsyncDriver ,
12- AsyncGraphDatabase ,
13- RoutingControl
14- )
10+ from neo4j import AsyncDriver , AsyncGraphDatabase , RoutingControl
1511from neo4j .exceptions import ClientError , Neo4jError
1612from pydantic import Field
1713
1814logger = logging .getLogger ("mcp_neo4j_cypher" )
1915
16+
2017def _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+
2927def _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
271293if __name__ == "__main__" :
0 commit comments