Skip to content

Commit 15aa55d

Browse files
authored
aura-manager - add logging to utils parsing functions, update tests (#172)
1 parent 487060e commit 15aa55d

File tree

4 files changed

+939
-44
lines changed

4 files changed

+939
-44
lines changed

servers/mcp-neo4j-cloud-aura-api/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
* Add security middleware (CORS and TrustedHost protection) for HTTP transport
1313
* Add `--allow-origins` and `--allowed-hosts` command line arguments
1414
* Add security environment variables: `NEO4J_MCP_SERVER_ALLOW_ORIGINS` and `NEO4J_MCP_SERVER_ALLOWED_HOSTS`
15+
* Update config parsing functions
16+
* Add clear logging for config declaration via cli and env variables
1517

1618
## v0.3.0
1719

servers/mcp-neo4j-cloud-aura-api/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ RUN pip install --no-cache-dir hatchling
1010
COPY pyproject.toml /app/
1111

1212
# Install runtime dependencies
13-
RUN pip install --no-cache-dir fastmcp>=2.0.0 requests>=2.31.0
13+
RUN pip install --no-cache-dir fastmcp>=2.0.0 requests>=2.31.0 starlette>=0.40.0
1414

1515
# Copy the source code
1616
COPY src/ /app/src/

servers/mcp-neo4j-cloud-aura-api/src/mcp_neo4j_aura_manager/utils.py

Lines changed: 306 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import logging
2+
import argparse
3+
import os
4+
from typing import Union, Literal
25

36
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
47

8+
ALLOWED_TRANSPORTS = ["stdio", "http", "sse"]
9+
510
def get_logger(name: str) -> logging.Logger:
611
"""Get a logger instance with consistent configuration."""
712
return logging.getLogger(name)
813

14+
logger = get_logger(__name__)
15+
916
def _validate_region(cloud_provider: str, region: str) -> None:
1017
"""
1118
Validate the region exists for the given cloud provider.
@@ -26,4 +33,302 @@ def _validate_region(cloud_provider: str, region: str) -> None:
2633
elif cloud_provider == "aws" and region.count("-") != 2:
2734
raise ValueError(f"Invalid region for AWS: {region}. Must follow the format 'region-zone-number'. Refer to https://neo4j.com/docs/aura/managing-instances/regions/ for valid regions.")
2835
elif cloud_provider == "azure" and region.count("-") != 0:
29-
raise ValueError(f"Invalid region for Azure: {region}. Must follow the format 'regionzone'. Refer to https://neo4j.com/docs/aura/managing-instances/regions/ for valid regions.")
36+
raise ValueError(f"Invalid region for Azure: {region}. Must follow the format 'regionzone'. Refer to https://neo4j.com/docs/aura/managing-instances/regions/ for valid regions.")
37+
38+
39+
def parse_client_id(args: argparse.Namespace) -> str:
40+
"""
41+
Parse the client id from the command line arguments or environment variables.
42+
43+
Parameters
44+
----------
45+
args : argparse.Namespace
46+
The command line arguments.
47+
48+
Returns
49+
-------
50+
client_id : str
51+
The client id.
52+
53+
Raises
54+
------
55+
ValueError: If no client id is provided.
56+
"""
57+
if args.client_id is not None:
58+
return args.client_id
59+
else:
60+
if os.getenv("NEO4J_AURA_CLIENT_ID") is not None:
61+
return os.getenv("NEO4J_AURA_CLIENT_ID")
62+
else:
63+
logger.error("Error: No Neo4j Aura Client ID provided. Please provide it as an argument or environment variable.")
64+
raise ValueError("No Neo4j Aura Client ID provided. Please provide it as an argument or environment variable.")
65+
66+
def parse_client_secret(args: argparse.Namespace) -> str:
67+
"""
68+
Parse the client secret from the command line arguments or environment variables.
69+
70+
Parameters
71+
----------
72+
args : argparse.Namespace
73+
The command line arguments.
74+
75+
Returns
76+
-------
77+
client_secret : str
78+
The client secret.
79+
80+
Raises
81+
------
82+
ValueError: If no client secret is provided.
83+
"""
84+
if args.client_secret is not None:
85+
return args.client_secret
86+
else:
87+
if os.getenv("NEO4J_AURA_CLIENT_SECRET") is not None:
88+
return os.getenv("NEO4J_AURA_CLIENT_SECRET")
89+
else:
90+
logger.error("Error: No Neo4j Aura Client Secret provided. Please provide it as an argument or environment variable.")
91+
raise ValueError("No Neo4j Aura Client Secret provided. Please provide it as an argument or environment variable.")
92+
93+
def parse_transport(args: argparse.Namespace) -> Literal["stdio", "http", "sse"]:
94+
"""
95+
Parse the transport from the command line arguments or environment variables.
96+
97+
Parameters
98+
----------
99+
args : argparse.Namespace
100+
The command line arguments.
101+
102+
Returns
103+
-------
104+
transport : str
105+
The transport.
106+
107+
Raises
108+
------
109+
ValueError: If no transport is provided or is invalid.
110+
"""
111+
112+
# parse transport
113+
if args.transport is not None:
114+
if args.transport not in ALLOWED_TRANSPORTS:
115+
logger.error(f"Invalid transport: {args.transport}. Allowed transports are: {ALLOWED_TRANSPORTS}")
116+
raise ValueError(f"Invalid transport: {args.transport}. Allowed transports are: {ALLOWED_TRANSPORTS}")
117+
return args.transport
118+
else:
119+
if os.getenv("NEO4J_TRANSPORT") is not None:
120+
if os.getenv("NEO4J_TRANSPORT") not in ALLOWED_TRANSPORTS:
121+
logger.error(f"Invalid transport: {os.getenv("NEO4J_TRANSPORT")}. Allowed transports are: {ALLOWED_TRANSPORTS}")
122+
raise ValueError(f"Invalid transport: {os.getenv("NEO4J_TRANSPORT")}. Allowed transports are: {ALLOWED_TRANSPORTS}")
123+
return os.getenv("NEO4J_TRANSPORT")
124+
else:
125+
logger.info("Info: No transport type provided. Using default: stdio")
126+
return "stdio"
127+
128+
def parse_server_host(args: argparse.Namespace, transport: Literal["stdio", "http", "sse"]) -> str:
129+
"""
130+
Parse the server host from the command line arguments or environment variables.
131+
132+
Parameters
133+
----------
134+
args : argparse.Namespace
135+
The command line arguments.
136+
transport : Literal["stdio", "http", "sse"]
137+
The transport.
138+
139+
Returns
140+
-------
141+
server_host : str
142+
The server host.
143+
"""
144+
# check cli argument
145+
if args.server_host is not None:
146+
if transport == "stdio":
147+
logger.warning("Warning: Server host provided, but transport is `stdio`. The `server_host` argument will be set, but ignored.")
148+
return args.server_host
149+
# check environment variable
150+
else:
151+
# if environment variable exists
152+
if os.getenv("NEO4J_MCP_SERVER_HOST") is not None:
153+
if transport == "stdio":
154+
logger.warning("Warning: Server host provided, but transport is `stdio`. The `NEO4J_MCP_SERVER_HOST` environment variable will be set, but ignored.")
155+
return os.getenv("NEO4J_MCP_SERVER_HOST")
156+
# if environment variable does not exist and not using stdio transport
157+
elif transport != "stdio":
158+
logger.warning("Warning: No server host provided and transport is not `stdio`. Using default server host: 127.0.0.1")
159+
return "127.0.0.1"
160+
# if environment variable does not exist and using stdio transport
161+
else:
162+
logger.info("Info: No server host provided and transport is `stdio`. `server_host` will be None.")
163+
return None
164+
165+
def parse_server_port(args: argparse.Namespace, transport: Literal["stdio", "http", "sse"]) -> int:
166+
"""
167+
Parse the server port from the command line arguments or environment variables.
168+
169+
Parameters
170+
----------
171+
args : argparse.Namespace
172+
The command line arguments.
173+
transport : Literal["stdio", "http", "sse"]
174+
The transport.
175+
176+
Returns
177+
-------
178+
server_port : int
179+
The server port.
180+
"""
181+
# check cli argument
182+
if args.server_port is not None:
183+
if transport == "stdio":
184+
logger.warning("Warning: Server port provided, but transport is `stdio`. The `server_port` argument will be set, but ignored.")
185+
return args.server_port
186+
# check environment variable
187+
else:
188+
# if environment variable exists
189+
if os.getenv("NEO4J_MCP_SERVER_PORT") is not None:
190+
if transport == "stdio":
191+
logger.warning("Warning: Server port provided, but transport is `stdio`. The `NEO4J_MCP_SERVER_PORT` environment variable will be set, but ignored.")
192+
return int(os.getenv("NEO4J_MCP_SERVER_PORT"))
193+
# if environment variable does not exist and not using stdio transport
194+
elif transport != "stdio":
195+
logger.warning("Warning: No server port provided and transport is not `stdio`. Using default server port: 8000")
196+
return 8000
197+
# if environment variable does not exist and using stdio transport
198+
else:
199+
logger.info("Info: No server port provided and transport is `stdio`. `server_port` will be None.")
200+
return None
201+
202+
def parse_server_path(args: argparse.Namespace, transport: Literal["stdio", "http", "sse"]) -> str:
203+
"""
204+
Parse the server path from the command line arguments or environment variables.
205+
206+
Parameters
207+
----------
208+
args : argparse.Namespace
209+
The command line arguments.
210+
transport : Literal["stdio", "http", "sse"]
211+
The transport.
212+
213+
Returns
214+
-------
215+
server_path : str
216+
The server path.
217+
"""
218+
# check cli argument
219+
if args.server_path is not None:
220+
if transport == "stdio":
221+
logger.warning("Warning: Server path provided, but transport is `stdio`. The `server_path` argument will be set, but ignored.")
222+
return args.server_path
223+
# check environment variable
224+
else:
225+
# if environment variable exists
226+
if os.getenv("NEO4J_MCP_SERVER_PATH") is not None:
227+
if transport == "stdio":
228+
logger.warning("Warning: Server path provided, but transport is `stdio`. The `NEO4J_MCP_SERVER_PATH` environment variable will be set, but ignored.")
229+
return os.getenv("NEO4J_MCP_SERVER_PATH")
230+
# if environment variable does not exist and not using stdio transport
231+
elif transport != "stdio":
232+
logger.warning("Warning: No server path provided and transport is not `stdio`. Using default server path: /mcp/")
233+
return "/mcp/"
234+
# if environment variable does not exist and using stdio transport
235+
else:
236+
logger.info("Info: No server path provided and transport is `stdio`. `server_path` will be None.")
237+
return None
238+
239+
def parse_allow_origins(args: argparse.Namespace) -> list[str]:
240+
"""
241+
Parse the allow origins from the command line arguments or environment variables.
242+
243+
Parameters
244+
----------
245+
args : argparse.Namespace
246+
The command line arguments.
247+
248+
Returns
249+
-------
250+
allow_origins : list[str]
251+
The allow origins.
252+
"""
253+
# check cli argument
254+
if args.allow_origins is not None:
255+
# Handle comma-separated string from CLI
256+
return [origin.strip() for origin in args.allow_origins.split(",") if origin.strip()]
257+
# check environment variable.
258+
else:
259+
if os.getenv("NEO4J_MCP_SERVER_ALLOW_ORIGINS") is not None:
260+
# split comma-separated string into list.
261+
return [
262+
origin.strip() for origin in os.getenv("NEO4J_MCP_SERVER_ALLOW_ORIGINS", "").split(",")
263+
if origin.strip()
264+
]
265+
else:
266+
logger.info("Info: No allow origins provided. Defaulting to no allowed origins.")
267+
return list()
268+
269+
def parse_allowed_hosts(args: argparse.Namespace) -> list[str]:
270+
"""
271+
Parse the allowed hosts from the command line arguments or environment variables.
272+
273+
Parameters
274+
----------
275+
args : argparse.Namespace
276+
The command line arguments.
277+
278+
Returns
279+
-------
280+
allowed_hosts : list[str]
281+
The allowed hosts.
282+
"""
283+
# check cli argument
284+
if args.allowed_hosts is not None:
285+
# Handle comma-separated string from CLI
286+
return [host.strip() for host in args.allowed_hosts.split(",") if host.strip()]
287+
288+
else:
289+
if os.getenv("NEO4J_MCP_SERVER_ALLOWED_HOSTS") is not None:
290+
# split comma-separated string into list
291+
return [
292+
host.strip() for host in os.getenv("NEO4J_MCP_SERVER_ALLOWED_HOSTS", "").split(",")
293+
if host.strip()
294+
]
295+
else:
296+
logger.info(
297+
"Info: No allowed hosts provided. Defaulting to secure mode - only localhost and 127.0.0.1 allowed."
298+
)
299+
return ["localhost", "127.0.0.1"]
300+
301+
def process_config(args: argparse.Namespace) -> dict[str, Union[str, int, None]]:
302+
"""
303+
Process the command line arguments and environment variables to create a config dictionary.
304+
This may then be used as input to the main server function.
305+
If any value is not provided, then a warning is logged and a default value is used, if appropriate.
306+
307+
Parameters
308+
----------
309+
args : argparse.Namespace
310+
The command line arguments.
311+
312+
Returns
313+
-------
314+
config : dict[str, str]
315+
The configuration dictionary.
316+
"""
317+
318+
config = dict()
319+
320+
# aura credentials
321+
config["client_id"] = parse_client_id(args)
322+
config["client_secret"] = parse_client_secret(args)
323+
324+
# server configuration
325+
config["transport"] = parse_transport(args)
326+
config["server_host"] = parse_server_host(args, config["transport"])
327+
config["server_port"] = parse_server_port(args, config["transport"])
328+
config["server_path"] = parse_server_path(args, config["transport"])
329+
330+
# middleware configuration
331+
config["allow_origins"] = parse_allow_origins(args)
332+
config["allowed_hosts"] = parse_allowed_hosts(args)
333+
334+
return config

0 commit comments

Comments
 (0)