Skip to content

Commit a73e190

Browse files
rfrneo4ja-s-g93
andauthored
aura-manager - add namespacing (#182)
* implemented tool namespacing support, updated docs and changelog, updated tests * update utils, tests test in claude desktop --------- Co-authored-by: runfourestrun <[email protected]> Co-authored-by: alex <[email protected]>
1 parent b097306 commit a73e190

File tree

8 files changed

+426
-23
lines changed

8 files changed

+426
-23
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Changed
66

77
### Added
8+
* Add namespacing support for multi-tenant deployments with `--namespace` CLI argument and `NEO4J_NAMESPACE` environment variable
89

910
## v0.4.2
1011

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,57 @@ Alternatively, you can set environment variables:
164164
}
165165
```
166166

167+
### 🏷️ Namespacing for Multi-tenant Deployments
168+
169+
The server supports namespacing to prefix tool names for multi-tenant deployments:
170+
171+
```json
172+
"mcpServers": {
173+
"neo4j-aura-app1": {
174+
"command": "uvx",
175+
"args": [
176+
177+
"--client-id", "<your-client-id>",
178+
"--client-secret", "<your-client-secret>",
179+
"--namespace", "app1"
180+
]
181+
},
182+
"neo4j-aura-app2": {
183+
"command": "uvx",
184+
"args": [
185+
186+
"--client-id", "<your-client-id>",
187+
"--client-secret", "<your-client-secret>",
188+
"--namespace", "app2"
189+
]
190+
}
191+
}
192+
```
193+
194+
#### CLI Usage
195+
```bash
196+
# With namespace
197+
mcp-neo4j-aura-manager --client-id <id> --client-secret <secret> --namespace myapp
198+
199+
# Tools become: myapp-list_instances, myapp-create_instance, etc.
200+
```
201+
202+
#### Environment Variables
203+
```bash
204+
export NEO4J_AURA_CLIENT_ID=your_client_id
205+
export NEO4J_AURA_CLIENT_SECRET=your_client_secret
206+
export NEO4J_NAMESPACE=myapp
207+
mcp-neo4j-aura-manager
208+
```
209+
210+
#### Docker with Namespacing
211+
```bash
212+
docker run -e NEO4J_AURA_CLIENT_ID=<id> \
213+
-e NEO4J_AURA_CLIENT_SECRET=<secret> \
214+
-e NEO4J_NAMESPACE=myapp \
215+
mcp-neo4j-aura-manager
216+
```
217+
167218
### 🌐 HTTP Transport Mode
168219

169220
The server supports HTTP transport for web-based deployments and microservices:
@@ -185,6 +236,7 @@ export NEO4J_MCP_SERVER_PORT=8080
185236
export NEO4J_MCP_SERVER_PATH=/api/mcp/
186237
export NEO4J_MCP_SERVER_ALLOWED_HOSTS="localhost,127.0.0.1"
187238
export NEO4J_MCP_SERVER_ALLOW_ORIGINS="http://localhost:3000"
239+
export NEO4J_NAMESPACE=myapp
188240
mcp-neo4j-aura-manager
189241
```
190242

@@ -330,6 +382,7 @@ docker run --rm -p 8000:8000 \
330382
| `NEO4J_MCP_SERVER_PATH` | `/mcp/` | Path for accessing MCP server |
331383
| `NEO4J_MCP_SERVER_ALLOW_ORIGINS` | _(empty - secure by default)_ | Comma-separated list of allowed CORS origins |
332384
| `NEO4J_MCP_SERVER_ALLOWED_HOSTS` | `localhost,127.0.0.1` | Comma-separated list of allowed hosts (DNS rebinding protection) |
385+
| `NEO4J_NAMESPACE` | _(empty - no prefix)_ | Namespace prefix for tool names (e.g., `myapp-list_instances`) |
333386

334387
### 🌐 SSE Transport for Legacy Web Access
335388

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def main():
2020
parser.add_argument("--client-secret", help="Neo4j Aura API Client Secret",
2121
default=os.environ.get("NEO4J_AURA_CLIENT_SECRET"))
2222
parser.add_argument("--transport", default=None, help="Transport type")
23+
parser.add_argument("--namespace", default=None, help="Tool namespace prefix")
2324
parser.add_argument("--server-host", default=None, help="Server host")
2425
parser.add_argument("--server-port", default=None, help="Server port")
2526
parser.add_argument("--server-path", default=None, help="Server path")
@@ -33,6 +34,7 @@ def main():
3334
default=None,
3435
help="Allowed hosts for DNS rebinding protection on remote servers(comma-separated list)",
3536
)
37+
3638

3739
args = parser.parse_args()
3840

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

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,22 @@
88
from starlette.middleware.trustedhost import TrustedHostMiddleware
99

1010
from .aura_manager import AuraManager
11-
from .utils import get_logger
11+
from .utils import get_logger, format_namespace
1212

1313
logger = get_logger(__name__)
1414

1515

16-
def create_mcp_server(aura_manager: AuraManager) -> FastMCP:
16+
17+
def create_mcp_server(aura_manager: AuraManager, namespace: str = "") -> FastMCP:
1718
"""Create an MCP server instance for Aura management."""
1819

20+
namespace_prefix = format_namespace(namespace)
21+
1922
mcp: FastMCP = FastMCP("mcp-neo4j-aura-manager", dependencies=["requests", "pydantic", "starlette"])
2023

21-
@mcp.tool(annotations=ToolAnnotations(title="List Instances",
24+
@mcp.tool(
25+
name=namespace_prefix + "list_instances",
26+
annotations=ToolAnnotations(title="List Instances",
2227
readOnlyHint=True,
2328
destructiveHint=False,
2429
idempotentHint=True,
@@ -30,19 +35,24 @@ async def list_instances() -> dict:
3035
result = await aura_manager.list_instances()
3136
return result
3237

33-
@mcp.tool(annotations=ToolAnnotations(title="Get Instance Details",
34-
readOnlyHint=True,
35-
destructiveHint=False,
36-
idempotentHint=True,
37-
openWorldHint=True
38+
@mcp.tool(
39+
name=namespace_prefix + "get_instance_details",
40+
annotations=ToolAnnotations(
41+
title="Get Instance Details",
42+
readOnlyHint=True,
43+
destructiveHint=False,
44+
idempotentHint=True,
45+
openWorldHint=True
3846
3947
))
4048
async def get_instance_details(instance_ids: List[str]) -> dict:
4149
"""Get details for one or more Neo4j Aura instances by ID."""
4250
result = await aura_manager.get_instance_details(instance_ids)
4351
return result
4452

45-
@mcp.tool(annotations=ToolAnnotations(title="Get Instance by Name",
53+
@mcp.tool(
54+
name=namespace_prefix + "get_instance_by_name",
55+
annotations=ToolAnnotations(title="Get Instance by Name",
4656
readOnlyHint=True,
4757
destructiveHint=False,
4858
idempotentHint=True,
@@ -54,7 +64,9 @@ async def get_instance_by_name(name: str) -> dict:
5464
result = await aura_manager.get_instance_by_name(name)
5565
return result
5666

57-
@mcp.tool(annotations=ToolAnnotations(title="Create Instance",
67+
@mcp.tool(
68+
name=namespace_prefix + "create_instance",
69+
annotations=ToolAnnotations(title="Create Instance",
5870
readOnlyHint=False,
5971
destructiveHint=False,
6072
idempotentHint=True,
@@ -86,7 +98,9 @@ async def create_instance(
8698
)
8799
return result
88100

89-
@mcp.tool(annotations=ToolAnnotations(title="Update Instance Name",
101+
@mcp.tool(
102+
name=namespace_prefix + "update_instance_name",
103+
annotations=ToolAnnotations(title="Update Instance Name",
90104
readOnlyHint=False,
91105
destructiveHint=True,
92106
idempotentHint=True,
@@ -98,7 +112,9 @@ async def update_instance_name(instance_id: str, name: str) -> dict:
98112
result = await aura_manager.update_instance_name(instance_id, name)
99113
return result
100114

101-
@mcp.tool(annotations=ToolAnnotations(title="Update Instance Memory",
115+
@mcp.tool(
116+
name=namespace_prefix + "update_instance_memory",
117+
annotations=ToolAnnotations(title="Update Instance Memory",
102118
readOnlyHint=False,
103119
destructiveHint=True,
104120
idempotentHint=True,
@@ -110,7 +126,8 @@ async def update_instance_memory(instance_id: str, memory: int) -> dict:
110126
result = await aura_manager.update_instance_memory(instance_id, memory)
111127
return result
112128

113-
@mcp.tool(annotations=ToolAnnotations(title="Update Instance Vector Optimization",
129+
@mcp.tool(name=namespace_prefix + "update_instance_vector_optimization",
130+
annotations=ToolAnnotations(title="Update Instance Vector Optimization",
114131
readOnlyHint=False,
115132
destructiveHint=True,
116133
idempotentHint=True,
@@ -122,7 +139,9 @@ async def update_instance_vector_optimization(instance_id: str, vector_optimized
122139
result = await aura_manager.update_instance_vector_optimization(instance_id, vector_optimized)
123140
return result
124141

125-
@mcp.tool(annotations=ToolAnnotations(title="Pause Instance",
142+
@mcp.tool(
143+
name=namespace_prefix + "pause_instance",
144+
annotations=ToolAnnotations(title="Pause Instance",
126145
readOnlyHint=False,
127146
destructiveHint=False,
128147
idempotentHint=True,
@@ -134,7 +153,9 @@ async def pause_instance(instance_id: str) -> dict:
134153
result = await aura_manager.pause_instance(instance_id)
135154
return result
136155

137-
@mcp.tool(annotations=ToolAnnotations(title="Resume Instance",
156+
@mcp.tool(
157+
name=namespace_prefix + "resume_instance",
158+
annotations=ToolAnnotations(title="Resume Instance",
138159
readOnlyHint=False,
139160
destructiveHint=False,
140161
idempotentHint=True,
@@ -146,7 +167,10 @@ async def resume_instance(instance_id: str) -> dict:
146167
result = await aura_manager.resume_instance(instance_id)
147168
return result
148169

149-
@mcp.tool(annotations=ToolAnnotations(title="List Tenants",
170+
171+
@mcp.tool(
172+
name=namespace_prefix + "list_tenants",
173+
annotations=ToolAnnotations(title="List Tenants",
150174
readOnlyHint=True,
151175
destructiveHint=False,
152176
idempotentHint=True,
@@ -158,7 +182,9 @@ async def list_tenants() -> dict:
158182
result = await aura_manager.list_tenants()
159183
return result
160184

161-
@mcp.tool(annotations=ToolAnnotations(title="Get Tenant Details",
185+
@mcp.tool(
186+
name=namespace_prefix + "get_tenant_details",
187+
annotations=ToolAnnotations(title="Get Tenant Details",
162188
readOnlyHint=True,
163189
destructiveHint=False,
164190
idempotentHint=True,
@@ -170,7 +196,8 @@ async def get_tenant_details(tenant_id: str) -> dict:
170196
result = await aura_manager.get_tenant_details(tenant_id)
171197
return result
172198

173-
@mcp.tool(annotations=ToolAnnotations(title="Delete Instance",
199+
@mcp.tool(name=namespace_prefix + "delete_instance",
200+
annotations=ToolAnnotations(title="Delete Instance",
174201
readOnlyHint=False,
175202
destructiveHint=True,
176203
idempotentHint=True,
@@ -189,6 +216,7 @@ async def main(
189216
client_id: str,
190217
client_secret: str,
191218
transport: Literal["stdio", "sse", "http"] = "stdio",
219+
namespace: str = "",
192220
host: str = "127.0.0.1",
193221
port: int = 8000,
194222
path: str = "/mcp/",
@@ -209,9 +237,9 @@ async def main(
209237
Middleware(TrustedHostMiddleware,
210238
allowed_hosts=allowed_hosts)
211239
]
212-
240+
213241
# Create MCP server
214-
mcp = create_mcp_server(aura_manager)
242+
mcp = create_mcp_server(aura_manager, namespace)
215243

216244
# Run the server with the specified transport
217245
match transport:

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ def get_logger(name: str) -> logging.Logger:
1313

1414
logger = get_logger(__name__)
1515

16+
17+
def format_namespace(namespace: str) -> str:
18+
if namespace:
19+
if namespace.endswith("-"):
20+
return namespace
21+
else:
22+
return namespace + "-"
23+
else:
24+
return ""
25+
1626
def _validate_region(cloud_provider: str, region: str) -> None:
1727
"""
1828
Validate the region exists for the given cloud provider.
@@ -298,6 +308,22 @@ def parse_allowed_hosts(args: argparse.Namespace) -> list[str]:
298308
)
299309
return ["localhost", "127.0.0.1"]
300310

311+
def parse_namespace(args: argparse.Namespace) -> str:
312+
"""
313+
Parse the namespace from the command line arguments or environment variables.
314+
"""
315+
# namespace configuration
316+
if args.namespace is not None:
317+
logger.info(f"Info: Namespace provided for tools: {args.namespace}")
318+
return args.namespace
319+
else:
320+
if os.getenv("NEO4J_NAMESPACE") is not None:
321+
logger.info(f"Info: Namespace provided for tools: {os.getenv('NEO4J_NAMESPACE')}")
322+
return os.getenv("NEO4J_NAMESPACE")
323+
else:
324+
logger.info("Info: No namespace provided for tools. No namespace will be used.")
325+
return ""
326+
301327
def process_config(args: argparse.Namespace) -> dict[str, Union[str, int, None]]:
302328
"""
303329
Process the command line arguments and environment variables to create a config dictionary.
@@ -331,4 +357,7 @@ def process_config(args: argparse.Namespace) -> dict[str, Union[str, int, None]]
331357
config["allow_origins"] = parse_allow_origins(args)
332358
config["allowed_hosts"] = parse_allowed_hosts(args)
333359

360+
# namespace configuration
361+
config["namespace"] = parse_namespace(args)
362+
334363
return config

servers/mcp-neo4j-cloud-aura-api/tests/integration/conftest.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ def run_server():
9595
port=8005,
9696
path="/mcp/",
9797
allow_origins=[], # Empty by default for security testing
98-
allowed_hosts=["localhost", "127.0.0.1"]
98+
allowed_hosts=["localhost", "127.0.0.1"],
99+
namespace="" # Add namespace parameter
99100
))
100101

101102
server_thread = threading.Thread(target=run_server, daemon=True)
@@ -131,7 +132,8 @@ def run_server():
131132
port=8006,
132133
path="/mcp/",
133134
allow_origins=["http://localhost:3000", "https://trusted-site.com"],
134-
allowed_hosts=["localhost", "127.0.0.1"]
135+
allowed_hosts=["localhost", "127.0.0.1"],
136+
namespace="" # Add namespace parameter
135137
))
136138

137139
server_thread = threading.Thread(target=run_server, daemon=True)

0 commit comments

Comments
 (0)