Skip to content

Commit 888da7b

Browse files
authored
memory - add security middleware (#169)
* add security middleware update config args, changelog, readme, tests based on Cypher MCP middleware implementation * add env defaults to dockerfile
1 parent 8e104e3 commit 888da7b

File tree

11 files changed

+1052
-442
lines changed

11 files changed

+1052
-442
lines changed

servers/mcp-neo4j-memory/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
* Change default transport to `stdio` in Dockerfile
77

88
### Added
9+
* Add env variable `NEO4J_MCP_SERVER_ALLOW_ORIGINS` and cli variable `--allow-origins` to configure CORS Middleware for remote deployments
10+
* Add env variable `NEO4J_MCP_SERVER_ALLOWED_HOSTS` and cli variable `--allowed-hosts` to configure Trusted Hosts Middleware for remote deployments
11+
* Update HTTP and SSE transports to use security middleware
12+
* Add comprehensive HTTP transport integration tests with security middleware testing
913

1014
## v0.3.0
1115

servers/mcp-neo4j-memory/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ ENV NEO4J_TRANSPORT="stdio"
2828
ENV NEO4J_MCP_SERVER_HOST="0.0.0.0"
2929
ENV NEO4J_MCP_SERVER_PORT="8000"
3030
ENV NEO4J_MCP_SERVER_PATH="/api/mcp/"
31+
ENV NEO4J_MCP_SERVER_ALLOW_ORIGINS=""
32+
ENV NEO4J_MCP_SERVER_ALLOWED_HOSTS="localhost,127.0.0.1"
3133

3234
# Command to run the server using the package entry point
3335
CMD ["sh", "-c", "mcp-neo4j-memory"]

servers/mcp-neo4j-memory/README.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,134 @@ The server supports three transport modes:
195195
}
196196
```
197197

198+
## 🔒 Security Protection
199+
200+
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.
201+
202+
### 🛡️ DNS Rebinding Protection
203+
204+
**TrustedHost Middleware** validates Host headers to prevent DNS rebinding attacks:
205+
206+
**Secure by Default:**
207+
- Only `localhost` and `127.0.0.1` hosts are allowed by default
208+
209+
**Environment Variable:**
210+
```bash
211+
export NEO4J_MCP_SERVER_ALLOWED_HOSTS="example.com,www.example.com"
212+
```
213+
214+
### 🌐 CORS Protection
215+
216+
**Cross-Origin Resource Sharing (CORS)** protection blocks browser-based requests by default:
217+
218+
**Environment Variable:**
219+
```bash
220+
export NEO4J_MCP_SERVER_ALLOW_ORIGINS="https://example.com,https://app.example.com"
221+
```
222+
223+
### 🔧 Complete Security Configuration
224+
225+
**Development Setup:**
226+
```bash
227+
mcp-neo4j-memory --transport http \
228+
--allowed-hosts "localhost,127.0.0.1" \
229+
--allow-origins "http://localhost:3000"
230+
```
231+
232+
**Production Setup:**
233+
```bash
234+
mcp-neo4j-memory --transport http \
235+
--allowed-hosts "example.com,www.example.com" \
236+
--allow-origins "https://example.com,https://app.example.com"
237+
```
238+
239+
### 🚨 Security Best Practices
240+
241+
**For `allow_origins`:**
242+
- Be specific: `["https://example.com", "https://example.com"]`
243+
- Never use `"*"` in production with credentials
244+
- Use HTTPS origins in production
245+
246+
**For `allowed_hosts`:**
247+
- Include your actual domain: `["example.com", "www.example.com"]`
248+
- Include localhost only for development
249+
- Never use `"*"` unless you understand the risks
250+
251+
## 🐳 Docker Deployment
252+
253+
The Neo4j Memory 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`.
254+
255+
### 📦 Using Your Built Image
256+
257+
After building locally with `docker build -t mcp-neo4j-memory:latest .`:
258+
259+
```bash
260+
# Run with http transport (default for Docker)
261+
docker run --rm -p 8000:8000 \
262+
-e NEO4J_URI="bolt://host.docker.internal:7687" \
263+
-e NEO4J_USERNAME="neo4j" \
264+
-e NEO4J_PASSWORD="password" \
265+
-e NEO4J_DATABASE="neo4j" \
266+
-e NEO4J_TRANSPORT="http" \
267+
-e NEO4J_MCP_SERVER_HOST="0.0.0.0" \
268+
-e NEO4J_MCP_SERVER_PORT="8000" \
269+
-e NEO4J_MCP_SERVER_PATH="/mcp/" \
270+
mcp/neo4j-memory:latest
271+
272+
# Run with security middleware for production
273+
docker run --rm -p 8000:8000 \
274+
-e NEO4J_URI="bolt://host.docker.internal:7687" \
275+
-e NEO4J_USERNAME="neo4j" \
276+
-e NEO4J_PASSWORD="password" \
277+
-e NEO4J_DATABASE="neo4j" \
278+
-e NEO4J_TRANSPORT="http" \
279+
-e NEO4J_MCP_SERVER_HOST="0.0.0.0" \
280+
-e NEO4J_MCP_SERVER_PORT="8000" \
281+
-e NEO4J_MCP_SERVER_PATH="/mcp/" \
282+
-e NEO4J_MCP_SERVER_ALLOWED_HOSTS="example.com,www.example.com" \
283+
-e NEO4J_MCP_SERVER_ALLOW_ORIGINS="https://example.com" \
284+
mcp/neo4j-memory:latest
285+
```
286+
287+
### 🔧 Environment Variables
288+
289+
| Variable | Default | Description |
290+
| ---------------------------------- | --------------------------------------- | -------------------------------------------------- |
291+
| `NEO4J_URI` | `bolt://localhost:7687` | Neo4j connection URI |
292+
| `NEO4J_USERNAME` | `neo4j` | Neo4j username |
293+
| `NEO4J_PASSWORD` | `password` | Neo4j password |
294+
| `NEO4J_DATABASE` | `neo4j` | Neo4j database name |
295+
| `NEO4J_TRANSPORT` | `stdio` (local), `http` (remote) | Transport protocol (`stdio`, `http`, or `sse`) |
296+
| `NEO4J_MCP_SERVER_HOST` | `127.0.0.1` (local) | Host to bind to |
297+
| `NEO4J_MCP_SERVER_PORT` | `8000` | Port for HTTP/SSE transport |
298+
| `NEO4J_MCP_SERVER_PATH` | `/mcp/` | Path for accessing MCP server |
299+
| `NEO4J_MCP_SERVER_ALLOW_ORIGINS` | _(empty - secure by default)_ | Comma-separated list of allowed CORS origins |
300+
| `NEO4J_MCP_SERVER_ALLOWED_HOSTS` | `localhost,127.0.0.1` | Comma-separated list of allowed hosts (DNS rebinding protection) |
301+
302+
### 🌐 SSE Transport for Legacy Web Access
303+
304+
When using SSE transport (for legacy web clients), the server exposes an HTTP endpoint:
305+
306+
```bash
307+
# Start the server with SSE transport
308+
docker run -d -p 8000:8000 \
309+
-e NEO4J_URI="neo4j+s://demo.neo4jlabs.com" \
310+
-e NEO4J_USERNAME="recommendations" \
311+
-e NEO4J_PASSWORD="recommendations" \
312+
-e NEO4J_DATABASE="neo4j" \
313+
-e NEO4J_TRANSPORT="sse" \
314+
-e NEO4J_MCP_SERVER_HOST="0.0.0.0" \
315+
-e NEO4J_MCP_SERVER_PORT="8000" \
316+
--name neo4j-memory-mcp-server \
317+
mcp-neo4j-memory:latest
318+
319+
# Test the SSE endpoint
320+
curl http://localhost:8000/sse
321+
322+
# Use with MCP Inspector
323+
npx @modelcontextprotocol/inspector http://localhost:8000/sse
324+
```
325+
198326
## 🚀 Development
199327

200328
### 📦 Prerequisites

servers/mcp-neo4j-memory/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ dependencies = [
88
"fastmcp>=2.0.0",
99
"neo4j>=5.26.0",
1010
"pydantic>=2.10.1",
11+
"starlette>=0.28.0",
1112
]
1213

1314
[build-system]

servers/mcp-neo4j-memory/src/mcp_neo4j_memory/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ def main():
1919
parser.add_argument("--server-host", default=None, help="HTTP host (default: 127.0.0.1)")
2020
parser.add_argument("--server-port", type=int, default=None, help="HTTP port (default: 8000)")
2121
parser.add_argument("--server-path", default=None, help="HTTP path (default: /mcp/)")
22+
parser.add_argument("--allow-origins", default=None, help="Comma-separated list of allowed CORS origins")
23+
parser.add_argument("--allowed-hosts", default=None, help="Comma-separated list of allowed hosts for DNS rebinding protection")
2224

2325
args = parser.parse_args()
2426
config = process_config(args)

servers/mcp-neo4j-memory/src/mcp_neo4j_memory/server.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
from neo4j import AsyncGraphDatabase
66
from pydantic import Field
7+
from starlette.middleware import Middleware
8+
from starlette.middleware.cors import CORSMiddleware
9+
from starlette.middleware.trustedhost import TrustedHostMiddleware
710

811
from fastmcp.server import FastMCP
912
from fastmcp.exceptions import ToolError
@@ -203,14 +206,16 @@ async def find_memories_by_name(names: list[str] = Field(..., description="List
203206

204207

205208
async def main(
206-
neo4j_uri: str,
207-
neo4j_user: str,
208-
neo4j_password: str,
209+
neo4j_uri: str,
210+
neo4j_user: str,
211+
neo4j_password: str,
209212
neo4j_database: str,
210213
transport: Literal["stdio", "sse", "http"] = "stdio",
211214
host: str = "127.0.0.1",
212215
port: int = 8000,
213216
path: str = "/mcp/",
217+
allow_origins: list[str] = [],
218+
allowed_hosts: list[str] = [],
214219
) -> None:
215220
logger.info(f"Starting Neo4j MCP Memory Server")
216221
logger.info(f"Connecting to Neo4j with DB URL: {neo4j_uri}")
@@ -237,6 +242,18 @@ async def main(
237242
# Create fulltext index
238243
await memory.create_fulltext_index()
239244

245+
# Configure security middleware
246+
custom_middleware = [
247+
Middleware(
248+
CORSMiddleware,
249+
allow_origins=allow_origins,
250+
allow_methods=["GET", "POST"],
251+
allow_headers=["*"],
252+
),
253+
Middleware(TrustedHostMiddleware,
254+
allowed_hosts=allowed_hosts)
255+
]
256+
240257
# Create MCP server
241258
mcp = create_mcp_server(memory)
242259
logger.info("MCP server created")
@@ -246,12 +263,12 @@ async def main(
246263
match transport:
247264
case "http":
248265
logger.info(f"HTTP server starting on {host}:{port}{path}")
249-
await mcp.run_http_async(host=host, port=port, path=path)
266+
await mcp.run_http_async(host=host, port=port, path=path, middleware=custom_middleware)
250267
case "stdio":
251268
logger.info("STDIO server starting")
252269
await mcp.run_stdio_async()
253270
case "sse":
254271
logger.info(f"SSE server starting on {host}:{port}{path}")
255-
await mcp.run_sse_async(host=host, port=port, path=path)
272+
await mcp.run_http_async(host=host, port=port, path=path, middleware=custom_middleware, transport="sse")
256273
case _:
257274
raise ValueError(f"Unsupported transport: {transport}")

servers/mcp-neo4j-memory/src/mcp_neo4j_memory/utils.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,41 @@ def process_config(args: argparse.Namespace) -> dict[str, Union[str, int, None]]
129129
logger.info("Info: No server path provided and transport is `stdio`. `server_path` will be None.")
130130
config["path"] = None
131131

132+
# parse allow origins
133+
if args.allow_origins is not None:
134+
# Handle comma-separated string from CLI
135+
136+
config["allow_origins"] = [origin.strip() for origin in args.allow_origins.split(",") if origin.strip()]
137+
138+
else:
139+
if os.getenv("NEO4J_MCP_SERVER_ALLOW_ORIGINS") is not None:
140+
# split comma-separated string into list
141+
config["allow_origins"] = [
142+
origin.strip() for origin in os.getenv("NEO4J_MCP_SERVER_ALLOW_ORIGINS", "").split(",")
143+
if origin.strip()
144+
]
145+
else:
146+
logger.info(
147+
"Info: No allow origins provided. Defaulting to no allowed origins."
148+
)
149+
config["allow_origins"] = list()
150+
151+
# parse allowed hosts for DNS rebinding protection
152+
if args.allowed_hosts is not None:
153+
# Handle comma-separated string from CLI
154+
config["allowed_hosts"] = [host.strip() for host in args.allowed_hosts.split(",") if host.strip()]
155+
156+
else:
157+
if os.getenv("NEO4J_MCP_SERVER_ALLOWED_HOSTS") is not None:
158+
# split comma-separated string into list
159+
config["allowed_hosts"] = [
160+
host.strip() for host in os.getenv("NEO4J_MCP_SERVER_ALLOWED_HOSTS", "").split(",")
161+
if host.strip()
162+
]
163+
else:
164+
logger.info(
165+
"Info: No allowed hosts provided. Defaulting to secure mode - only localhost and 127.0.0.1 allowed."
166+
)
167+
config["allowed_hosts"] = ["localhost", "127.0.0.1"]
168+
132169
return config

0 commit comments

Comments
 (0)