Skip to main content
Glama
quickstart.md20.4 kB
# Quick Start: Implementing MCP Server Issues Resolution **Feature**: 008-fix-issues-identified **Target Developers**: Backend engineers familiar with FastAPI and Python **Estimated Time**: 4-6 hours for all three issues --- ## Overview This guide provides step-by-step instructions for implementing fixes to three issues identified in the MCP server test report: 1. **404 vs 401 Priority** - Return 404 for non-existent routes (not 401) 2. **Rate Limit Headers** - Add industry-standard rate limit headers to responses 3. **API Key Generation** - Create CLI script for generating test API keys --- ## Prerequisites ✅ Python 3.12+ ✅ FastAPI 0.100+ project setup ✅ Existing middleware: `MCPAuthMiddleware`, `RateLimiterMiddleware` ✅ Supabase database with `api_keys` table ✅ Development environment: `uv` or `pip` for package management --- ## Issue 1: Fix 404 vs 401 Priority ### Problem Currently, requests to non-existent routes return `401 Unauthorized` instead of `404 Not Found` because authentication middleware executes before route matching. ### Solution Add a custom 404 exception handler that bypasses authentication. ### Implementation Steps **Step 1: Open `src/api/main.py`** Locate the FastAPI app initialization: ```python app = FastAPI( title="Hostaway MCP Server", version="0.1.0", # ... ) ``` **Step 2: Add 404 Exception Handler** Add this function after middleware setup: ```python from fastapi import Request, status from fastapi.exceptions import HTTPException from fastapi.responses import JSONResponse @app.exception_handler(404) async def custom_404_handler(request: Request, exc: HTTPException) -> JSONResponse: """ Return 404 for non-existent routes without requiring authentication. This handler is invoked BEFORE authentication middleware, ensuring that clients receive accurate HTTP status codes even without valid credentials. """ return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, content={"detail": f"Route '{request.url.path}' not found"}, headers={ "X-Correlation-ID": getattr(request.state, "correlation_id", "unknown"), "Access-Control-Allow-Origin": "*", # Maintain CORS }, ) ``` **Step 3: Test the Change** ```bash # Test non-existent route (should return 404) curl -i http://localhost:8000/api/nonexistent # Expected: HTTP/1.1 404 Not Found # Test existing route without auth (should return 401) curl -i http://localhost:8000/api/listings # Expected: HTTP/1.1 401 Unauthorized # Test with valid auth (should work) curl -i -H "X-API-Key: mcp_..." http://localhost:8000/api/listings # Expected: HTTP/1.1 200 OK ``` **Step 4: Add Integration Test** Create `tests/integration/test_404_vs_401_priority.py`: ```python import pytest from fastapi.testclient import TestClient from src.api.main import app client = TestClient(app) def test_nonexistent_route_returns_404(): """Non-existent routes should return 404, not 401.""" response = client.get("/api/nonexistent") assert response.status_code == 404 assert "not found" in response.json()["detail"].lower() def test_existing_route_requires_auth(): """Existing routes should still return 401 without auth.""" response = client.get("/api/listings") assert response.status_code == 401 assert "api key" in response.json()["detail"].lower() def test_existing_route_with_invalid_auth(): """Invalid auth should return 401, not 404.""" response = client.get( "/api/listings", headers={"X-API-Key": "invalid-key"} ) assert response.status_code == 401 def test_405_method_not_allowed_still_works(): """Unsupported methods should return 405, not 404 or 401.""" response = client.delete("/health") # DELETE not allowed on /health assert response.status_code == 405 ``` --- ## Issue 2: Add Rate Limit Headers ### Problem Rate limiting is implemented but not transparent to clients. No headers indicate limit status. ### Solution Extend `RateLimiterMiddleware` to add `X-RateLimit-*` headers to all responses. ### Implementation Steps **Step 1: Open `src/api/middleware/rate_limiter.py`** Locate the `RateLimiterMiddleware` class: ```python class RateLimiterMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): # Existing rate limit check logic... pass ``` **Step 2: Add Header Logic After Rate Limit Check** Update the `dispatch` method to attach headers: ```python from datetime import datetime, timezone from typing import Dict, Any class RateLimiterMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): # Existing rate limit check logic... # (Get client identifier, check limits, etc.) # Calculate rate limit info client_id = self._get_client_identifier(request) limit_info = self._get_rate_limit_info(client_id, request) # Store in request state for header addition request.state.rate_limit_info = limit_info # Process request response = await call_next(request) # Add rate limit headers to response if hasattr(request.state, "rate_limit_info"): info = request.state.rate_limit_info response.headers["X-RateLimit-Limit"] = str(info["limit"]) response.headers["X-RateLimit-Remaining"] = str(info["remaining"]) response.headers["X-RateLimit-Reset"] = str(info["reset_time"]) return response def _get_rate_limit_info(self, client_id: str, request: Request) -> Dict[str, Any]: """ Calculate current rate limit status for client. Returns: { "limit": 15 or 20 (IP-based or account-based), "remaining": requests remaining in window, "reset_time": Unix timestamp when window resets } """ # Determine limit type is_authenticated = hasattr(request.state, "organization_id") limit = 20 if is_authenticated else 15 window_seconds = 10 # Get current usage from in-memory state state = self._rate_limit_state.get(client_id, { "count": 0, "window_start": datetime.now(timezone.utc).timestamp() }) # Calculate remaining and reset time current_time = datetime.now(timezone.utc).timestamp() time_elapsed = current_time - state["window_start"] if time_elapsed > window_seconds: # Window expired, reset remaining = limit reset_time = int(current_time + window_seconds) else: # Within window remaining = max(0, limit - state["count"]) reset_time = int(state["window_start"] + window_seconds) return { "limit": limit, "remaining": remaining, "reset_time": reset_time } ``` **Step 3: Update 429 Response Handler** Ensure 429 responses also include headers: ```python # In rate limit check logic, when limit exceeded: if state["count"] >= limit: info = self._get_rate_limit_info(client_id, request) return JSONResponse( status_code=429, content={"detail": "Rate limit exceeded"}, headers={ "X-RateLimit-Limit": str(info["limit"]), "X-RateLimit-Remaining": "0", "X-RateLimit-Reset": str(info["reset_time"]), "Retry-After": str(info["reset_time"] - int(datetime.now(timezone.utc).timestamp())) } ) ``` **Step 4: Test the Change** ```bash # Make authenticated request, check headers curl -i -H "X-API-Key: mcp_..." http://localhost:8000/health # Expected headers: # X-RateLimit-Limit: 20 # X-RateLimit-Remaining: 19 # X-RateLimit-Reset: 1698765432 # Make multiple requests to trigger rate limit for i in {1..25}; do curl -i -H "X-API-Key: mcp_..." http://localhost:8000/health done # Last response should be 429 with remaining=0 ``` **Step 5: Add Unit Tests** Create `tests/unit/test_rate_limiter_headers.py`: ```python import pytest from datetime import datetime, timezone from src.api.middleware.rate_limiter import RateLimiterMiddleware def test_rate_limit_info_calculation(): """Test rate limit info calculation logic.""" middleware = RateLimiterMiddleware(app=None) # Mock request state class MockRequest: state = type('obj', (object,), {'organization_id': 123})() request = MockRequest() client_id = "test-client" # Get rate limit info info = middleware._get_rate_limit_info(client_id, request) # Assertions assert info["limit"] == 20 # Authenticated assert 0 <= info["remaining"] <= 20 assert info["reset_time"] > datetime.now(timezone.utc).timestamp() def test_rate_limit_headers_present(): """Test headers are added to response.""" # Use TestClient to make request from fastapi.testclient import TestClient from src.api.main import app client = TestClient(app) response = client.get("/health") # Check headers present assert "X-RateLimit-Limit" in response.headers assert "X-RateLimit-Remaining" in response.headers assert "X-RateLimit-Reset" in response.headers def test_rate_limit_headers_decrement(): """Test remaining count decrements correctly.""" client = TestClient(app) # First request response1 = client.get("/health") remaining1 = int(response1.headers["X-RateLimit-Remaining"]) # Second request response2 = client.get("/health") remaining2 = int(response2.headers["X-RateLimit-Remaining"]) # Remaining should decrement assert remaining2 == remaining1 - 1 ``` --- ## Issue 3: Create API Key Generation Script ### Problem No documented way to generate test API keys for local development. ### Solution Create a CLI script using Click framework for interactive key generation. ### Implementation Steps **Step 1: Install Click (if not already installed)** ```bash uv add click>=8.1.0 # or pip install click>=8.1.0 ``` **Step 2: Create `src/scripts/generate_api_key.py`** ```python """ API Key Generation Script for Hostaway MCP Server Usage: python -m src.scripts.generate_api_key --org-id 999 --user-id <uuid> Generates a test API key with format: mcp_{base64_urlsafe_32_chars} Stores SHA-256 hash in Supabase api_keys table. """ import secrets import hashlib import sys from typing import Optional import click from supabase import create_client, Client from pydantic import ValidationError @click.command() @click.option( '--org-id', type=int, prompt='Organization ID', help='Organization to associate key with (must exist in organizations table)' ) @click.option( '--user-id', type=str, prompt='User UUID', help='UUID of user creating the key (must exist in auth.users table)' ) @click.option( '--supabase-url', type=str, envvar='SUPABASE_URL', default='http://127.0.0.1:54321', help='Supabase URL (default: local instance)' ) @click.option( '--supabase-key', type=str, envvar='SUPABASE_SERVICE_KEY', prompt=True, hide_input=True, help='Supabase service key (will prompt if not provided)' ) def generate_key(org_id: int, user_id: str, supabase_url: str, supabase_key: str): """Generate a test API key for local development.""" click.echo("\n🔑 Generating API Key...") click.echo("-" * 50) # Step 1: Generate cryptographically secure random token token = secrets.token_urlsafe(32) api_key = f"mcp_{token}" # Step 2: Compute SHA-256 hash for storage key_hash = hashlib.sha256(api_key.encode()).hexdigest() click.echo(f"✓ Key generated: {api_key[:15]}...{api_key[-10:]}") click.echo(f"✓ Hash computed: {key_hash[:16]}...") # Step 3: Validate organization exists try: supabase: Client = create_client(supabase_url, supabase_key) click.echo(f"\n🔍 Validating organization {org_id}...") org_check = supabase.table("organizations").select("id").eq("id", org_id).execute() if not org_check.data: click.echo(f"❌ Error: Organization {org_id} not found", err=True) click.echo(" Create organization first or use existing ID") sys.exit(1) click.echo(f"✓ Organization {org_id} exists") except Exception as e: click.echo(f"❌ Error connecting to Supabase: {e}", err=True) sys.exit(1) # Step 4: Insert into api_keys table try: click.echo(f"\n💾 Inserting key into database...") result = supabase.table("api_keys").insert({ "organization_id": org_id, "key_hash": key_hash, "created_by_user_id": user_id, "is_active": True }).execute() click.echo(f"✓ Key inserted with ID: {result.data[0]['id']}") except Exception as e: click.echo(f"❌ Error inserting key: {e}", err=True) sys.exit(1) # Step 5: Display success and usage instructions click.echo("\n" + "=" * 50) click.echo("✅ API Key Generated Successfully!") click.echo("=" * 50) click.echo(f"\n🔑 Your API Key (copy this):") click.echo(f" {api_key}") click.echo(f"\n📝 Hash (for verification):") click.echo(f" {key_hash}") click.echo(f"\n🚀 Usage in API requests:") click.echo(f" curl -H 'X-API-Key: {api_key}' http://localhost:8000/api/listings") click.echo(f"\n📋 Key Details:") click.echo(f" Organization ID: {org_id}") click.echo(f" Created By: {user_id}") click.echo(f" Status: Active") click.echo("\n⚠️ Store this key securely - it cannot be retrieved later!") click.echo() if __name__ == '__main__': generate_key() ``` **Step 3: Create Documentation** Create `docs/API_KEY_GENERATION.md`: ```markdown # API Key Generation Guide ## Quick Start Generate a test API key for local development: \`\`\`bash # Using environment variables export SUPABASE_URL="http://127.0.0.1:54321" export SUPABASE_SERVICE_KEY="your-service-key" python -m src.scripts.generate_api_key --org-id 999 --user-id <uuid> \`\`\` ## Prerequisites 1. Supabase running (local or remote) 2. Organization created in `organizations` table 3. User created in `auth.users` table 4. Supabase service key (from Supabase dashboard) ## Step-by-Step Setup ### 1. Start Supabase (Local Development) \`\`\`bash supabase start \`\`\` ### 2. Create Test Organization \`\`\`bash psql postgresql://postgres:postgres@127.0.0.1:54322/postgres <<EOF INSERT INTO organizations (id, name, owner_user_id) VALUES (999, 'Test Organization', '00000000-0000-0000-0000-000000000001') ON CONFLICT (id) DO NOTHING; EOF \`\`\` ### 3. Generate API Key \`\`\`bash python -m src.scripts.generate_api_key \ --org-id 999 \ --user-id 00000000-0000-0000-0000-000000000001 \`\`\` ### 4. Test API Key \`\`\`bash curl -H "X-API-Key: mcp_..." http://localhost:8000/health \`\`\` ## Troubleshooting **Error: Organization not found** - Create organization first (see step 2 above) **Error: User not found** - Create user in auth.users table or use existing UUID **Error: Connection refused** - Check Supabase is running: `supabase status` \`\`\` **Step 4: Test the Script** ```bash # Run script python -m src.scripts.generate_api_key \ --org-id 999 \ --user-id 00000000-0000-0000-0000-000000000001 # Expected output: # 🔑 Generating API Key... # ✓ Key generated: mcp_C_Fo9M7N5wV... # ✓ Organization 999 exists # ✓ Key inserted # ✅ API Key Generated Successfully! ``` **Step 5: Add Unit Tests** Create `tests/unit/test_api_key_generation.py`: ```python import pytest import hashlib import re from src.scripts.generate_api_key import generate_key from click.testing import CliRunner def test_api_key_format(): """Test generated key follows mcp_{base64} format.""" runner = CliRunner() result = runner.invoke(generate_key, [ '--org-id', '999', '--user-id', '00000000-0000-0000-0000-000000000001', '--supabase-url', 'http://localhost:54321', '--supabase-key', 'test-key' ]) # Extract key from output key_pattern = r'mcp_[A-Za-z0-9_-]{32,}' matches = re.findall(key_pattern, result.output) assert len(matches) > 0, "No API key found in output" api_key = matches[0] # Validate format assert api_key.startswith('mcp_') assert len(api_key) > 36 # mcp_ (4 chars) + 32+ chars def test_sha256_hash_computation(): """Test SHA-256 hash is computed correctly.""" test_key = "mcp_test123456789" expected_hash = hashlib.sha256(test_key.encode()).hexdigest() # Script should compute same hash computed_hash = hashlib.sha256(test_key.encode()).hexdigest() assert computed_hash == expected_hash ``` --- ## Testing All Fixes Together ### Integration Test Suite Create `tests/integration/test_all_fixes.py`: ```python import pytest from fastapi.testclient import TestClient from src.api.main import app client = TestClient(app) def test_404_returns_before_auth_check(): """Test 1: 404 for non-existent routes.""" response = client.get("/api/nonexistent") assert response.status_code == 404 def test_rate_limit_headers_present(): """Test 2: Rate limit headers in responses.""" response = client.get("/health") assert "X-RateLimit-Limit" in response.headers assert "X-RateLimit-Remaining" in response.headers assert "X-RateLimit-Reset" in response.headers def test_api_key_works_after_generation(): """Test 3: Generated API key works in requests.""" # Generate key using script from src.scripts.generate_api_key import generate_key # ... (invoke script) # Use key in request response = client.get( "/api/listings", headers={"X-API-Key": "mcp_generated_key_here"} ) assert response.status_code == 200 ``` ### Performance Test Create `tests/performance/test_middleware_performance.py`: ```python import pytest import time from fastapi.testclient import TestClient from src.api.main import app def test_middleware_performance_no_regression(): """Ensure middleware changes don't degrade performance.""" client = TestClient(app) # Baseline: measure 100 requests start = time.time() for _ in range(100): response = client.get("/health") assert response.status_code == 200 elapsed = time.time() - start # Average response time should be < 5ms (500ms / 100) avg_time = elapsed / 100 assert avg_time < 0.005, f"Performance regression: {avg_time*1000:.2f}ms per request" ``` --- ## Deployment Checklist Before merging: - [ ] All unit tests pass (`pytest tests/unit/`) - [ ] All integration tests pass (`pytest tests/integration/`) - [ ] Performance tests pass (`pytest tests/performance/`) - [ ] Type checking passes (`mypy --strict src/`) - [ ] Linting passes (`ruff check src/`) - [ ] Documentation updated (`docs/API_KEY_GENERATION.md`) - [ ] Pre-commit hooks pass - [ ] Manual testing completed: - [ ] 404 for non-existent routes - [ ] Rate limit headers visible in curl - [ ] API key script works end-to-end --- ## Troubleshooting **Issue: 404 handler not triggering** - Check handler is registered AFTER middleware setup - Verify exception handler syntax: `@app.exception_handler(404)` **Issue: Rate limit headers missing** - Check `request.state.rate_limit_info` is set before `call_next()` - Verify headers are added AFTER response is generated **Issue: API key script fails** - Check Supabase is running: `supabase status` - Verify organization and user exist in database - Check service key has proper permissions --- ## Estimated Time Breakdown | Task | Time | Difficulty | |------|------|------------| | Issue 1: 404 handler | 1 hour | Easy | | Issue 2: Rate limit headers | 2 hours | Medium | | Issue 3: API key script | 2 hours | Medium | | Testing (all issues) | 1 hour | Easy | | **Total** | **6 hours** | **Medium** | --- ## Next Steps After implementation: 1. Run `/speckit.tasks` to generate detailed task breakdown 2. Execute tasks in dependency order 3. Run full test suite before merging 4. Update CHANGELOG.md with fixes 5. Deploy to staging for validation 6. Monitor production metrics after deployment --- **Questions?** See [research.md](./research.md) for detailed technical decisions and alternatives considered.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/darrentmorgan/hostaway-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server