1- import tiktoken
2-
31import argparse
42import logging
53import os
64from typing import Any , Union
75
6+ import tiktoken
7+
88logger = logging .getLogger ("mcp_neo4j_cypher" )
99logger .setLevel (logging .INFO )
1010
1111
12+ def parse_boolean_safely (value : Union [str , bool ]) -> bool :
13+ """
14+ Safely parse a string value to boolean with strict validation.
15+
16+ Parameters
17+ ----------
18+ value : Union[str, bool]
19+ The value to parse to boolean.
20+
21+ Returns
22+ -------
23+ bool
24+ The parsed boolean value.
25+ """
26+
27+ if isinstance (value , bool ):
28+ return value
29+
30+ elif isinstance (value , str ):
31+ normalized = value .strip ().lower ()
32+ if normalized == "true" :
33+ return True
34+ elif normalized == "false" :
35+ return False
36+ else :
37+ raise ValueError (
38+ f"Invalid boolean value: '{ value } '. Must be 'true' or 'false'"
39+ )
40+ # we shouldn't get here, but just in case
41+ else :
42+ raise ValueError (f"Invalid boolean value: '{ value } '. Must be 'true' or 'false'" )
43+
44+
1245def process_config (args : argparse .Namespace ) -> dict [str , Union [str , int , None ]]:
1346 """
1447 Process the command line arguments and environment variables to create a config dictionary.
@@ -173,14 +206,17 @@ def process_config(args: argparse.Namespace) -> dict[str, Union[str, int, None]]
173206 # parse allow origins
174207 if args .allow_origins is not None :
175208 # Handle comma-separated string from CLI
176-
177- config ["allow_origins" ] = [origin .strip () for origin in args .allow_origins .split ("," ) if origin .strip ()]
209+
210+ config ["allow_origins" ] = [
211+ origin .strip () for origin in args .allow_origins .split ("," ) if origin .strip ()
212+ ]
178213
179214 else :
180215 if os .getenv ("NEO4J_MCP_SERVER_ALLOW_ORIGINS" ) is not None :
181216 # split comma-separated string into list
182217 config ["allow_origins" ] = [
183- origin .strip () for origin in os .getenv ("NEO4J_MCP_SERVER_ALLOW_ORIGINS" , "" ).split ("," )
218+ origin .strip ()
219+ for origin in os .getenv ("NEO4J_MCP_SERVER_ALLOW_ORIGINS" , "" ).split ("," )
184220 if origin .strip ()
185221 ]
186222 else :
@@ -192,21 +228,24 @@ def process_config(args: argparse.Namespace) -> dict[str, Union[str, int, None]]
192228 # parse allowed hosts for DNS rebinding protection
193229 if args .allowed_hosts is not None :
194230 # Handle comma-separated string from CLI
195- config ["allowed_hosts" ] = [host .strip () for host in args .allowed_hosts .split ("," ) if host .strip ()]
196-
231+ config ["allowed_hosts" ] = [
232+ host .strip () for host in args .allowed_hosts .split ("," ) if host .strip ()
233+ ]
234+
197235 else :
198236 if os .getenv ("NEO4J_MCP_SERVER_ALLOWED_HOSTS" ) is not None :
199237 # split comma-separated string into list
200238 config ["allowed_hosts" ] = [
201- host .strip () for host in os .getenv ("NEO4J_MCP_SERVER_ALLOWED_HOSTS" , "" ).split ("," )
239+ host .strip ()
240+ for host in os .getenv ("NEO4J_MCP_SERVER_ALLOWED_HOSTS" , "" ).split ("," )
202241 if host .strip ()
203242 ]
204243 else :
205244 logger .info (
206245 "Info: No allowed hosts provided. Defaulting to secure mode - only localhost and 127.0.0.1 allowed."
207246 )
208247 config ["allowed_hosts" ] = ["localhost" , "127.0.0.1" ]
209-
248+
210249 # parse token limit
211250 if args .token_limit is not None :
212251 config ["token_limit" ] = args .token_limit
@@ -232,12 +271,31 @@ def process_config(args: argparse.Namespace) -> dict[str, Union[str, int, None]]
232271 )
233272 config ["read_timeout" ] = config ["read_timeout" ]
234273 except ValueError :
235- logger .warning ("Warning: Invalid read timeout provided. Using default: 30 seconds" )
274+ logger .warning (
275+ "Warning: Invalid read timeout provided. Using default: 30 seconds"
276+ )
236277 config ["read_timeout" ] = 30
237278 else :
238279 logger .info ("Info: No read timeout provided. Using default: 30 seconds" )
239280 config ["read_timeout" ] = 30
240281
282+ # parse read-only
283+ if args .read_only :
284+ config ["read_only" ] = True
285+ logger .info (
286+ f"Info: Read-only mode set to { config ['read_only' ]} via command line argument."
287+ )
288+ elif os .getenv ("NEO4J_READ_ONLY" ) is not None :
289+ config ["read_only" ] = parse_boolean_safely (os .getenv ("NEO4J_READ_ONLY" ))
290+ logger .info (
291+ f"Info: Read-only mode set to { config ['read_only' ]} via environment variable."
292+ )
293+ else :
294+ logger .info (
295+ "Info: No read-only setting provided. Write queries will be allowed."
296+ )
297+ config ["read_only" ] = False
298+
241299 return config
242300
243301
@@ -295,6 +353,7 @@ def _value_sanitize(d: Any, list_limit: int = 128) -> Any:
295353 else :
296354 return d
297355
356+
298357def _truncate_string_to_tokens (
299358 text : str , token_limit : int , model : str = "gpt-4"
300359) -> str :
0 commit comments