Skip to content

Commit 713ae5d

Browse files
Stephen PeterkinsStephen Peterkins
authored andcommitted
refactor: improve auth middleware for MCP operational endpoints
1 parent a07eafd commit 713ae5d

File tree

4 files changed

+90
-42
lines changed

4 files changed

+90
-42
lines changed

examples/server_with_custom_routes.py

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from north_mcp_python_sdk import NorthMCPServer
55
from north_mcp_python_sdk.auth import get_authenticated_user, get_authenticated_user_optional
66

7-
mcp = NorthMCPServer("Demo with Custom Routes", port=5222)
7+
# MCP server with operational endpoints for container orchestration
8+
mcp = NorthMCPServer("MCP Server with K8s Endpoints", port=5222)
89

910

1011
@mcp.tool()
@@ -21,45 +22,71 @@ def add(a: int, b: int) -> int:
2122

2223
@mcp.custom_route("/health", methods=["GET"])
2324
async def health_check(request: Request) -> PlainTextResponse:
24-
"""Health check endpoint - no authentication required"""
25+
"""Kubernetes liveness probe endpoint - automatically bypasses authentication"""
2526
return PlainTextResponse("OK")
2627

2728

28-
@mcp.custom_route("/status", methods=["GET"])
29-
async def status_check(request: Request) -> JSONResponse:
30-
"""Status endpoint - no authentication required"""
29+
@mcp.custom_route("/ready", methods=["GET"])
30+
async def readiness_check(request: Request) -> JSONResponse:
31+
"""Kubernetes readiness probe - checks if server is ready to accept traffic"""
32+
# In a real implementation, you might check database connections,
33+
# external service availability, etc.
3134
return JSONResponse({
32-
"status": "running",
33-
"server": "NorthMCP Demo",
34-
"authenticated": False
35+
"status": "ready",
36+
"server": "MCP Server with K8s Endpoints",
37+
"checks": {
38+
"mcp_protocol": "ok",
39+
"tools_loaded": "ok"
40+
}
3541
})
3642

3743

38-
@mcp.custom_route("/user-info", methods=["GET"])
39-
async def user_info(request: Request) -> JSONResponse:
40-
"""User info endpoint that shows auth is optional for custom routes"""
44+
@mcp.custom_route("/metrics", methods=["GET"])
45+
async def metrics_endpoint(request: Request) -> PlainTextResponse:
46+
"""Prometheus metrics endpoint for monitoring"""
47+
# In a real implementation, you would return actual Prometheus metrics
48+
metrics = """# HELP mcp_requests_total Total number of MCP requests
49+
# TYPE mcp_requests_total counter
50+
mcp_requests_total 42
51+
52+
# HELP mcp_tools_total Number of available MCP tools
53+
# TYPE mcp_tools_total gauge
54+
mcp_tools_total 1
55+
"""
56+
return PlainTextResponse(metrics, media_type="text/plain")
57+
58+
59+
@mcp.custom_route("/status", methods=["GET"])
60+
async def status_check(request: Request) -> JSONResponse:
61+
"""General status endpoint for monitoring dashboards"""
4162
user = get_authenticated_user_optional()
4263

64+
# This endpoint works without auth but can show auth info if provided
65+
status_data = {
66+
"status": "running",
67+
"server": "MCP Server with K8s Endpoints",
68+
"version": "1.0.0",
69+
"uptime_seconds": 3600, # In real implementation, track actual uptime
70+
"authenticated_request": user is not None
71+
}
72+
4373
if user:
44-
return JSONResponse({
45-
"authenticated": True,
46-
"email": user.email,
47-
"connectors": list(user.connector_access_tokens.keys())
48-
})
49-
else:
50-
return JSONResponse({
51-
"authenticated": False,
52-
"message": "This custom route works without authentication!"
53-
})
74+
status_data["user_email"] = user.email
75+
status_data["available_connectors"] = list(user.connector_access_tokens.keys())
76+
77+
return JSONResponse(status_data)
5478

5579

5680
if __name__ == "__main__":
57-
print("Starting server with custom routes...")
58-
print("Try these endpoints:")
59-
print(" GET /health - Simple health check")
60-
print(" GET /status - Status information")
61-
print(" GET /user-info - Shows authentication is optional")
62-
print(" POST /mcp - MCP protocol endpoint (requires auth)")
63-
print(" GET /sse - Server-sent events endpoint (requires auth)")
81+
print("Starting MCP server with Kubernetes operational endpoints...")
82+
print("\nOperational endpoints (no authentication required):")
83+
print(" GET /health - Liveness probe for Kubernetes")
84+
print(" GET /ready - Readiness probe for Kubernetes")
85+
print(" GET /metrics - Prometheus metrics for monitoring")
86+
print(" GET /status - General status for dashboards")
87+
print("\nMCP protocol endpoints (authentication required):")
88+
print(" POST /mcp - JSON-RPC MCP communication")
89+
print(" GET /sse - Server-sent events for streaming")
90+
print("\nPerfect for deployment in Kubernetes with proper health checks!")
6491

6592
mcp.run(transport="streamable-http")

examples/simple_custom_route.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,24 @@
22
from starlette.requests import Request
33
from starlette.responses import PlainTextResponse
44

5-
# Create your North MCP server
6-
mcp = NorthMCPServer("MyServer")
5+
# Create MCP server ready for Kubernetes deployment
6+
mcp = NorthMCPServer("MyMCPServer")
77

8-
# Add a custom route - no auth middleware applied automatically!
8+
# Add health check for Kubernetes liveness probe
9+
# Custom routes automatically bypass authentication - perfect for operational endpoints!
910
@mcp.custom_route("/health", methods=["GET"])
1011
async def health_check(request: Request) -> PlainTextResponse:
12+
"""Kubernetes liveness probe endpoint"""
1113
return PlainTextResponse("OK")
1214

13-
# That's it! Custom routes bypass auth, MCP routes (/mcp, /sse) require auth
15+
@mcp.tool()
16+
def my_tool(message: str) -> str:
17+
"""Example MCP tool - accessible via authenticated /mcp endpoint"""
18+
return f"Processed: {message}"
19+
20+
# Deploy with confidence: /health works without auth, /mcp requires auth
1421
if __name__ == "__main__":
22+
print("MCP server ready for Kubernetes deployment!")
23+
print("Health check: GET /health (no auth)")
24+
print("MCP protocol: POST /mcp (requires auth)")
1525
mcp.run()

src/north_mcp_python_sdk/auth.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,20 @@ def __init__(
3333

3434
class NorthAuthenticationMiddleware:
3535
"""
36-
North's default authentication middleware that only applies authentication
37-
to MCP protocol paths (/mcp, /sse). Custom routes bypass authentication entirely.
36+
North's authentication middleware for MCP servers that applies authentication
37+
only to MCP protocol endpoints (/mcp, /sse). Custom routes bypass authentication
38+
and are intended for operational purposes like Kubernetes health checks.
3839
39-
This is the standard authentication behavior for North MCP servers:
40-
- MCP protocol routes (/mcp, /sse) require authentication
41-
- All custom routes added via @mcp.custom_route() work without authentication
42-
- No configuration needed - this behavior is automatic
40+
MCP servers typically only need two authenticated endpoints:
41+
- /mcp: JSON-RPC protocol endpoint for MCP communication
42+
- /sse: Server-sent events endpoint for streaming transport
43+
44+
Custom routes are automatically public and designed for:
45+
- Kubernetes liveness/readiness probes (/health, /ready)
46+
- Monitoring and metrics endpoints (/metrics, /status)
47+
- Other operational/orchestration needs
48+
49+
No configuration needed - this behavior follows MCP best practices.
4350
"""
4451

4552
def __init__(
@@ -65,7 +72,7 @@ def _should_authenticate(self, path: str) -> bool:
6572
Check if the given path requires authentication.
6673
Only MCP protocol paths (/mcp, /sse) require auth by default.
6774
"""
68-
return any(path.startswith(protected) for protected in self.protected_paths)
75+
return path in self.protected_paths
6976

7077
async def __call__(self, scope: Scope, receive: Receive, send: Send):
7178
if scope["type"] == "lifespan":
@@ -74,13 +81,17 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send):
7481
path = scope.get("path", "")
7582

7683
if not self._should_authenticate(path):
77-
self.logger.debug("Path %s does not require authentication, bypassing", path)
84+
self.logger.debug(
85+
"Path %s is a custom route (likely operational endpoint like health check), "
86+
"bypassing authentication as intended for k8s/orchestration use",
87+
path
88+
)
7889
# For non-protected paths, create a minimal unauthenticated user
7990
scope["user"] = None
8091
scope["auth"] = AuthCredentials()
8192
return await self.app(scope, receive, send)
8293

83-
self.logger.debug("Path %s requires authentication", path)
94+
self.logger.debug("Path %s is an MCP protocol endpoint, applying authentication", path)
8495

8596
# Apply authentication for protected paths
8697
conn = HTTPConnection(scope)
@@ -147,7 +158,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send):
147158

148159
# For custom routes that don't require auth, user will be None
149160
if user is None:
150-
self.logger.debug("No authentication required for this route")
161+
self.logger.debug("Custom route accessed without authentication (operational endpoint)")
151162
token = auth_context_var.set(None)
152163
try:
153164
await self.app(scope, receive, send)
File renamed without changes.

0 commit comments

Comments
 (0)