Skip to content

Commit 487060e

Browse files
authored
aura-manager - add security middleware to aura server (#171)
1 parent 458c7b4 commit 487060e

File tree

10 files changed

+526
-44
lines changed

10 files changed

+526
-44
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
### Added
1111
* Add tool annotations to tools to better describe their effects
12+
* Add security middleware (CORS and TrustedHost protection) for HTTP transport
13+
* Add `--allow-origins` and `--allowed-hosts` command line arguments
14+
* Add security environment variables: `NEO4J_MCP_SERVER_ALLOW_ORIGINS` and `NEO4J_MCP_SERVER_ALLOWED_HOSTS`
1215

1316
## v0.3.0
1417

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ ENV NEO4J_TRANSPORT="stdio"
2626
ENV NEO4J_MCP_SERVER_HOST="127.0.0.1"
2727
ENV NEO4J_MCP_SERVER_PORT=8000
2828
ENV NEO4J_MCP_SERVER_PATH="/mcp/"
29+
ENV NEO4J_MCP_SERVER_ALLOW_ORIGINS=""
30+
ENV NEO4J_MCP_SERVER_ALLOWED_HOSTS="localhost,127.0.0.1"
2931

3032
# Command to run the server using the package entry point
3133
CMD ["sh", "-c", "mcp-neo4j-aura-manager"]

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
.PHONY: docker-local-build-run install-dev test-unit test-integration test-http test-all all
2+
3+
docker-local-build-run:
4+
docker build -t mcp-neo4j-aura-manager .
5+
docker run -p 8000:8000 mcp-neo4j-aura-manager:latest
6+
17
install-dev:
28
uv sync
39

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

Lines changed: 177 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ export NEO4J_TRANSPORT=http
183183
export NEO4J_MCP_SERVER_HOST=127.0.0.1
184184
export NEO4J_MCP_SERVER_PORT=8080
185185
export NEO4J_MCP_SERVER_PATH=/api/mcp/
186+
export NEO4J_MCP_SERVER_ALLOWED_HOSTS="localhost,127.0.0.1"
187+
export NEO4J_MCP_SERVER_ALLOW_ORIGINS="http://localhost:3000"
186188
mcp-neo4j-aura-manager
187189
```
188190

@@ -191,9 +193,183 @@ mcp-neo4j-aura-manager
191193
The server supports three transport modes:
192194

193195
- **STDIO** (default): Standard input/output for local tools and Claude Desktop
194-
- **SSE**: Server-Sent Events for web-based deployments
196+
- **SSE**: Server-Sent Events for web-based deployments
195197
- **HTTP**: Streamable HTTP for modern web deployments and microservices
196198

199+
## 🔒 Security Protection
200+
201+
The server includes comprehensive security protection with **secure defaults** that protect against common web-based attacks while preserving full MCP functionality when using HTTP transport.
202+
203+
### 🛡️ DNS Rebinding Protection
204+
205+
**TrustedHost Middleware** validates Host headers to prevent DNS rebinding attacks:
206+
207+
**Secure by Default:**
208+
- Only `localhost` and `127.0.0.1` hosts are allowed by default
209+
- Malicious websites cannot trick browsers into accessing your local server
210+
211+
**Environment Variable:**
212+
```bash
213+
export NEO4J_MCP_SERVER_ALLOWED_HOSTS="example.com,www.example.com"
214+
```
215+
216+
### 🌐 CORS Protection
217+
218+
**Cross-Origin Resource Sharing (CORS)** protection blocks browser-based requests by default:
219+
220+
**Environment Variable:**
221+
```bash
222+
export NEO4J_MCP_SERVER_ALLOW_ORIGINS="https://example.com,https://example.com"
223+
```
224+
225+
### 🔧 Complete Security Configuration
226+
227+
**Development Setup:**
228+
```bash
229+
mcp-neo4j-aura-manager --transport http \
230+
--allowed-hosts "localhost,127.0.0.1" \
231+
--allow-origins "http://localhost:3000"
232+
```
233+
234+
**Production Setup:**
235+
```bash
236+
mcp-neo4j-aura-manager --transport http \
237+
--allowed-hosts "example.com,www.example.com" \
238+
--allow-origins "https://example.com,https://example.com"
239+
```
240+
241+
### 🚨 Security Best Practices
242+
243+
**For `allow_origins`:**
244+
- Be specific: `["https://example.com", "https://example.com"]`
245+
- Never use `"*"` in production with credentials
246+
- Use HTTPS origins in production
247+
248+
**For `allowed_hosts`:**
249+
- Include your actual domain: `["example.com", "www.example.com"]`
250+
- Include localhost only for development
251+
- Never use `"*"` unless you understand the risks
252+
253+
254+
## 🐳 Docker Deployment
255+
256+
The Neo4j Aura Manager MCP server can be deployed using Docker for remote deployments. Docker deployment should use HTTP transport for web accessibility. In order to integrate this deployment with applications like Claude Desktop, you will have to use a proxy in your MCP configuration such as `mcp-remote`.
257+
258+
### 🐳 Using with Docker for Claude Desktop
259+
260+
Here we use the Docker Hub hosted Aura Manager MCP server image with stdio transport for use with Claude Desktop.
261+
262+
**Config details:**
263+
* `-i`: Interactive mode - keeps STDIN open for stdio transport communication
264+
* `--rm`: Automatically remove container when it exits (cleanup)
265+
* `-p 8000:8000`: Port mapping - maps host port 8000 to container port 8000
266+
* `NEO4J_TRANSPORT=stdio`: Uses stdio transport for Claude Desktop compatibility
267+
* `NEO4J_AURA_CLIENT_ID` and `NEO4J_AURA_CLIENT_SECRET`: Your Aura API credentials
268+
269+
```json
270+
{
271+
"mcpServers": {
272+
"neo4j-aura": {
273+
"command": "docker",
274+
"args": [
275+
"run",
276+
"-i",
277+
"--rm",
278+
"-p",
279+
"8000:8000",
280+
"-e", "NEO4J_AURA_CLIENT_ID=your-client-id",
281+
"-e", "NEO4J_AURA_CLIENT_SECRET=your-client-secret",
282+
"-e", "NEO4J_TRANSPORT=stdio",
283+
"mcp/neo4j-aura-manager:latest"
284+
]
285+
}
286+
}
287+
}
288+
```
289+
290+
### 📦 Using Your Built Image
291+
292+
After building locally with `docker build -t mcp-neo4j-aura-manager:latest .`:
293+
294+
```bash
295+
# Build the image
296+
docker build -t mcp-neo4j-aura-manager:<version> .
297+
298+
# Run with http transport (default for Docker)
299+
docker run --rm -p 8000:8000 \
300+
-e NEO4J_AURA_CLIENT_ID="your-client-id" \
301+
-e NEO4J_AURA_CLIENT_SECRET="your-client-secret" \
302+
-e NEO4J_TRANSPORT="http" \
303+
-e NEO4J_MCP_SERVER_HOST="0.0.0.0" \
304+
-e NEO4J_MCP_SERVER_PORT="8000" \
305+
-e NEO4J_MCP_SERVER_PATH="/mcp/" \
306+
mcp-neo4j-aura-manager:<version>
307+
308+
# Run with security middleware for production
309+
docker run --rm -p 8000:8000 \
310+
-e NEO4J_AURA_CLIENT_ID="your-client-id" \
311+
-e NEO4J_AURA_CLIENT_SECRET="your-client-secret" \
312+
-e NEO4J_TRANSPORT="http" \
313+
-e NEO4J_MCP_SERVER_HOST="0.0.0.0" \
314+
-e NEO4J_MCP_SERVER_PORT="8000" \
315+
-e NEO4J_MCP_SERVER_PATH="/mcp/" \
316+
-e NEO4J_MCP_SERVER_ALLOWED_HOSTS="example.com,www.example.com" \
317+
-e NEO4J_MCP_SERVER_ALLOW_ORIGINS="https://example.com" \
318+
mcp-neo4j-aura-manager:<version>
319+
```
320+
321+
### 🔧 Environment Variables
322+
323+
| Variable | Default | Description |
324+
| ---------------------------------- | --------------------------------------- | -------------------------------------------------- |
325+
| `NEO4J_AURA_CLIENT_ID` | _(none)_ | Neo4j Aura API Client ID |
326+
| `NEO4J_AURA_CLIENT_SECRET` | _(none)_ | Neo4j Aura API Client Secret |
327+
| `NEO4J_TRANSPORT` | `stdio` (local), `http` (remote) | Transport protocol (`stdio`, `http`, or `sse`) |
328+
| `NEO4J_MCP_SERVER_HOST` | `127.0.0.1` (local) | Host to bind to |
329+
| `NEO4J_MCP_SERVER_PORT` | `8000` | Port for HTTP/SSE transport |
330+
| `NEO4J_MCP_SERVER_PATH` | `/mcp/` | Path for accessing MCP server |
331+
| `NEO4J_MCP_SERVER_ALLOW_ORIGINS` | _(empty - secure by default)_ | Comma-separated list of allowed CORS origins |
332+
| `NEO4J_MCP_SERVER_ALLOWED_HOSTS` | `localhost,127.0.0.1` | Comma-separated list of allowed hosts (DNS rebinding protection) |
333+
334+
### 🌐 SSE Transport for Legacy Web Access
335+
336+
When using SSE transport (for legacy web clients), the server exposes an HTTP endpoint:
337+
338+
```bash
339+
# Start the server with SSE transport
340+
docker run -d -p 8000:8000 \
341+
-e NEO4J_AURA_CLIENT_ID="your-client-id" \
342+
-e NEO4J_AURA_CLIENT_SECRET="your-client-secret" \
343+
-e NEO4J_TRANSPORT="sse" \
344+
-e NEO4J_MCP_SERVER_HOST="0.0.0.0" \
345+
-e NEO4J_MCP_SERVER_PORT="8000" \
346+
--name neo4j-aura-mcp-server \
347+
mcp-neo4j-aura-manager:latest
348+
349+
# Test the SSE endpoint
350+
curl http://localhost:8000/sse
351+
352+
# Use with MCP Inspector
353+
npx @modelcontextprotocol/inspector http://localhost:8000/sse
354+
```
355+
356+
### 🔗 Claude Desktop Integration with Docker
357+
358+
For Claude Desktop integration with a Dockerized server using http transport:
359+
360+
```json
361+
{
362+
"mcpServers": {
363+
"neo4j-aura-docker": {
364+
"command": "npx",
365+
"args": ["-y", "mcp-remote@latest", "http://localhost:8000/mcp/"]
366+
}
367+
}
368+
}
369+
```
370+
371+
**Note**: First start your Docker container with HTTP transport, then Claude Desktop can connect to it via the HTTP endpoint and proxy server like `mcp-remote`.
372+
197373
## 📝 Usage Examples
198374

199375
### 🔍 Give overview over my tenants
@@ -245,20 +421,6 @@ source .venv/bin/activate # On Unix/macOS
245421
uv pip install -e ".[dev]"
246422
```
247423

248-
### 🐳 Docker
249-
250-
Build and run the Docker container:
251-
252-
```bash
253-
# Build the image
254-
docker build -t mcp-neo4j-aura-manager:<version> .
255-
256-
# Run the container
257-
docker run -e NEO4J_AURA_CLIENT_ID="your-client-id" \
258-
-e NEO4J_AURA_CLIENT_SECRET="your-client-secret" \
259-
mcp-neo4j-aura-manager:<version>
260-
```
261-
262424
## 📄 License
263425

264426
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.

servers/mcp-neo4j-cloud-aura-api/pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ requires-python = ">=3.10"
77
dependencies = [
88
"fastmcp>=2.0.0",
99
"requests>=2.31.0",
10+
"starlette>=0.40.0",
1011
]
1112

1213
[build-system]
@@ -28,3 +29,8 @@ mcp-neo4j-aura-manager = "mcp_neo4j_aura_manager:main"
2829
pythonpath = [
2930
"src"
3031
]
32+
asyncio_mode = "strict"
33+
asyncio_default_fixture_loop_scope = "session"
34+
markers = [
35+
"middleware: marks tests as middleware security tests that don't require Aura credentials"
36+
]

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,40 @@ def main():
2222
parser.add_argument("--server-host", default=None, help="Server host")
2323
parser.add_argument("--server-port", default=None, help="Server port")
2424
parser.add_argument("--server-path", default=None, help="Server path")
25+
parser.add_argument(
26+
"--allow-origins",
27+
default=None,
28+
help="Allow origins for remote servers (comma-separated list)",
29+
)
30+
parser.add_argument(
31+
"--allowed-hosts",
32+
default=None,
33+
help="Allowed hosts for DNS rebinding protection on remote servers(comma-separated list)",
34+
)
2535

2636
args = parser.parse_args()
2737

2838
if not args.client_id or not args.client_secret:
2939
logger.error("Client ID and Client Secret are required. Provide them as arguments or environment variables.")
3040
sys.exit(1)
3141

42+
# Parse security arguments
43+
allow_origins_str = args.allow_origins or os.getenv("NEO4J_MCP_SERVER_ALLOW_ORIGINS", "")
44+
allow_origins = [origin.strip() for origin in allow_origins_str.split(",") if origin.strip()] if allow_origins_str else []
45+
46+
allowed_hosts_str = args.allowed_hosts or os.getenv("NEO4J_MCP_SERVER_ALLOWED_HOSTS", "localhost,127.0.0.1")
47+
allowed_hosts = [host.strip() for host in allowed_hosts_str.split(",") if host.strip()] if allowed_hosts_str else []
48+
3249
try:
3350
asyncio.run(server.main(
34-
args.client_id,
51+
args.client_id,
3552
args.client_secret,
3653
args.transport or os.getenv("NEO4J_TRANSPORT", "stdio"),
3754
args.server_host or os.getenv("NEO4J_MCP_SERVER_HOST", "127.0.0.1"),
38-
args.server_port or os.getenv("NEO4J_MCP_SERVER_PORT", 8000),
55+
int(args.server_port or os.getenv("NEO4J_MCP_SERVER_PORT", 8000)),
3956
args.server_path or os.getenv("NEO4J_MCP_SERVER_PATH", "/mcp/"),
57+
allow_origins,
58+
allowed_hosts,
4059
))
4160
except KeyboardInterrupt:
4261
logger.info("Server stopped by user")

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

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
from fastmcp.server import FastMCP
44
from mcp.types import ToolAnnotations
55
from pydantic import Field
6+
from starlette.middleware import Middleware
7+
from starlette.middleware.cors import CORSMiddleware
8+
from starlette.middleware.trustedhost import TrustedHostMiddleware
69

710
from .aura_manager import AuraManager
811
from .utils import get_logger
@@ -13,7 +16,7 @@
1316
def create_mcp_server(aura_manager: AuraManager) -> FastMCP:
1417
"""Create an MCP server instance for Aura management."""
1518

16-
mcp: FastMCP = FastMCP("mcp-neo4j-aura-manager", dependencies=["requests", "pydantic"], stateless_http=True)
19+
mcp: FastMCP = FastMCP("mcp-neo4j-aura-manager", dependencies=["requests", "pydantic", "starlette"])
1720

1821
@mcp.tool(annotations=ToolAnnotations(title="List Instances",
1922
readOnlyHint=True,
@@ -183,31 +186,57 @@ async def delete_instance(instance_id: str) -> dict:
183186

184187

185188
async def main(
186-
client_id: str,
189+
client_id: str,
187190
client_secret: str,
188191
transport: Literal["stdio", "sse", "http"] = "stdio",
189192
host: str = "127.0.0.1",
190193
port: int = 8000,
191194
path: str = "/mcp/",
195+
allow_origins: list[str] = [],
196+
allowed_hosts: list[str] = [],
192197
) -> None:
193198
"""Start the MCP server."""
194199
logger.info("Starting MCP Neo4j Aura Manager Server")
195200

196201
aura_manager = AuraManager(client_id, client_secret)
197-
202+
custom_middleware = [
203+
Middleware(
204+
CORSMiddleware,
205+
allow_origins=allow_origins,
206+
allow_methods=["GET", "POST"],
207+
allow_headers=["*"],
208+
),
209+
Middleware(TrustedHostMiddleware,
210+
allowed_hosts=allowed_hosts)
211+
]
212+
198213
# Create MCP server
199214
mcp = create_mcp_server(aura_manager)
200215

201216
# Run the server with the specified transport
202217
match transport:
203218
case "http":
204-
await mcp.run_http_async(host=host, port=port, path=path)
219+
logger.info(
220+
f"Running Neo4j Aura Manager MCP Server with HTTP transport on {host}:{port}..."
221+
)
222+
await mcp.run_http_async(
223+
host=host, port=port, path=path, middleware=custom_middleware, stateless_http=True
224+
)
205225
case "stdio":
226+
logger.info("Running Neo4j Aura Manager MCP Server with stdio transport...")
206227
await mcp.run_stdio_async()
207228
case "sse":
208-
await mcp.run_sse_async(host=host, port=port, path=path)
229+
logger.info(
230+
f"Running Neo4j Aura Manager MCP Server with SSE transport on {host}:{port}..."
231+
)
232+
await mcp.run_http_async(host=host, port=port, path=path, middleware=custom_middleware, transport="sse", stateless_http=True)
209233
case _:
210-
raise ValueError(f"Unsupported transport: {transport}")
234+
logger.error(
235+
f"Invalid transport: {transport} | Must be either 'stdio', 'sse', or 'http'"
236+
)
237+
raise ValueError(
238+
f"Invalid transport: {transport} | Must be either 'stdio', 'sse', or 'http'"
239+
)
211240

212241

213242
if __name__ == "__main__":

0 commit comments

Comments
 (0)