Skip to content

Commit 34e33f8

Browse files
committed
fix: PORT range validation, MCP auth middleware, last_used_at tracking
- config.py: PORT now validated 1-65535, falls back to 8080 if out of range - config.py: added MCP_AUTH_TOKEN env var - server.py: Bearer token middleware on /mcp endpoint when MCP_AUTH_TOKEN set; /health remains public; no auth → 401, wrong token → 401 - auth.py: _validate_api_key now updates last_used_at on successful lookup (fire-and-forget, won't block auth on failure) - 004_api_keys.sql: documented DELETE policy intent (soft-delete via active flag) - ci.yml: supabase/migrations/** added to mcp path filter
1 parent b1edc43 commit 34e33f8

6 files changed

Lines changed: 43 additions & 3 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ jobs:
3131
- 'frontend/**'
3232
mcp:
3333
- 'mcp-server/**'
34+
- 'supabase/migrations/**'
3435
3536
test-backend:
3637
name: Backend Tests

backend/middleware/auth.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,16 @@ def _validate_api_key(token: str) -> Optional[AuthContext]:
122122
return None
123123

124124
key_data = result.data[0]
125+
126+
# Update last_used_at timestamp (fire-and-forget, don't block auth)
127+
try:
128+
from datetime import datetime, timezone
129+
db.table("api_keys").update(
130+
{"last_used_at": datetime.now(timezone.utc).isoformat()}
131+
).eq("id", key_data["id"]).execute()
132+
except Exception:
133+
pass # Non-critical; don't fail auth over timestamp update
134+
125135
return AuthContext(
126136
api_key_name=key_data.get("name"),
127137
user_id=key_data.get("user_id"),

mcp-server/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@ MCP_TRANSPORT=stdio
88
# Server host/port (only used for streamable-http transport)
99
MCP_HOST=0.0.0.0
1010
PORT=8080
11+
12+
# Auth token for protecting /mcp endpoint (optional, recommended for remote)
13+
# If set, clients must send Authorization: Bearer <token>
14+
MCP_AUTH_TOKEN=

mcp-server/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,11 @@
2121
_port_raw = os.getenv("PORT", "8080")
2222
try:
2323
PORT = int(_port_raw)
24+
if not (1 <= PORT <= 65535):
25+
PORT = 8080
2426
except ValueError:
2527
PORT = 8080
28+
29+
# Optional auth token for protecting the MCP endpoint in remote mode.
30+
# If set, clients must send Authorization: Bearer <token> to /mcp.
31+
MCP_AUTH_TOKEN = os.getenv("MCP_AUTH_TOKEN", "")

mcp-server/server.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from starlette.routing import Route
1717
from starlette.responses import JSONResponse
1818

19-
from config import SERVER_NAME, SERVER_VERSION, TRANSPORT, HOST, PORT
19+
from config import SERVER_NAME, SERVER_VERSION, TRANSPORT, HOST, PORT, MCP_AUTH_TOKEN
2020
from tools import get_tool_schemas
2121
from handlers import call_tool
2222

@@ -64,8 +64,25 @@ async def _health(request):
6464

6565
def _get_http_app():
6666
"""Build the Starlette app with health check + MCP endpoint."""
67+
from starlette.middleware.base import BaseHTTPMiddleware
68+
from starlette.requests import Request
69+
6770
app = mcp.streamable_http_app()
6871
app.routes.insert(0, Route("/health", _health, methods=["GET"]))
72+
73+
if MCP_AUTH_TOKEN:
74+
class MCPAuthMiddleware(BaseHTTPMiddleware):
75+
"""Require Bearer token on /mcp, leave /health public."""
76+
async def dispatch(self, request: Request, call_next):
77+
if request.url.path == "/health":
78+
return await call_next(request)
79+
auth = request.headers.get("authorization", "")
80+
if not auth.startswith("Bearer ") or auth[7:] != MCP_AUTH_TOKEN:
81+
return JSONResponse({"error": "Unauthorized"}, status_code=401)
82+
return await call_next(request)
83+
84+
app.add_middleware(MCPAuthMiddleware)
85+
6986
return app
7087

7188

supabase/migrations/004_api_keys.sql

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,7 @@ CREATE POLICY "Users can deactivate own keys"
6666
USING (auth.uid() = user_id)
6767
WITH CHECK (auth.uid() = user_id);
6868

69-
-- Service role (backend) has full access via service_role key
70-
-- No explicit policy needed; service_role bypasses RLS
69+
-- No DELETE policy: users cannot hard-delete keys.
70+
-- Deactivation (active=false) is the intended revocation mechanism.
71+
-- Service role (backend) can hard-delete for compliance if needed.
72+
-- Service role has full access via service_role key; bypasses RLS.

0 commit comments

Comments
 (0)