Commit
·
b338c26
1
Parent(s):
70cf1f6
added api key based authentication
Browse files- README.md +146 -17
- app.py +161 -22
- database/api_keys.py +261 -0
- generate_api_key.py +142 -0
- requirements.txt +0 -1
- server.py +26 -90
README.md
CHANGED
|
@@ -72,6 +72,7 @@ FleetMind is a production-ready **Model Context Protocol (MCP) server** that tra
|
|
| 72 |
|
| 73 |
### Key Features
|
| 74 |
|
|
|
|
| 75 |
✅ **29 AI Tools** - Order, Driver & Assignment Management (including Gemini 2.0 Flash AI)
|
| 76 |
✅ **2 Real-Time Resources** - Live data feeds (orders://all, drivers://all)
|
| 77 |
✅ **Google Maps Integration** - Geocoding & Route Calculation with traffic data
|
|
@@ -155,30 +156,70 @@ FleetMind isn't just an MCP server—it's a **blueprint for enterprise AI integr
|
|
| 155 |
|
| 156 |
### Connect from Claude Desktop
|
| 157 |
|
| 158 |
-
|
| 159 |
|
| 160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
**For Production (HuggingFace Space):**
|
| 163 |
```json
|
| 164 |
{
|
| 165 |
"mcpServers": {
|
| 166 |
-
"
|
| 167 |
"command": "npx",
|
| 168 |
"args": [
|
| 169 |
"mcp-remote",
|
| 170 |
"https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse"
|
| 171 |
-
]
|
|
|
|
|
|
|
|
|
|
| 172 |
}
|
| 173 |
}
|
| 174 |
}
|
| 175 |
```
|
| 176 |
|
| 177 |
-
**
|
|
|
|
|
|
|
| 178 |
```json
|
| 179 |
{
|
| 180 |
"mcpServers": {
|
| 181 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
"command": "npx",
|
| 183 |
"args": [
|
| 184 |
"mcp-remote",
|
|
@@ -188,26 +229,35 @@ FleetMind isn't just an MCP server—it's a **blueprint for enterprise AI integr
|
|
| 188 |
}
|
| 189 |
}
|
| 190 |
```
|
|
|
|
| 191 |
|
| 192 |
**Both (Production + Local):**
|
| 193 |
```json
|
| 194 |
{
|
| 195 |
"mcpServers": {
|
| 196 |
-
"
|
| 197 |
"command": "npx",
|
| 198 |
-
"args": ["mcp-remote", "http://localhost:7860/sse"]
|
|
|
|
|
|
|
|
|
|
| 199 |
},
|
| 200 |
-
"
|
| 201 |
"command": "npx",
|
| 202 |
-
"args": ["mcp-remote", "https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse"]
|
|
|
|
|
|
|
|
|
|
| 203 |
}
|
| 204 |
}
|
| 205 |
}
|
| 206 |
```
|
| 207 |
|
| 208 |
-
|
|
|
|
|
|
|
| 209 |
|
| 210 |
-
|
| 211 |
- "Create an urgent delivery order for Sarah at 456 Oak Ave, San Francisco"
|
| 212 |
- "Use intelligent AI assignment to find the best driver for this order"
|
| 213 |
- "Show me all available drivers"
|
|
@@ -472,6 +522,7 @@ CREATE TABLE drivers (
|
|
| 472 |
- Python 3.10+
|
| 473 |
- PostgreSQL database (or use Neon serverless)
|
| 474 |
- Google Maps API key
|
|
|
|
| 475 |
|
| 476 |
### Setup
|
| 477 |
|
|
@@ -488,17 +539,40 @@ cp .env.example .env
|
|
| 488 |
# Edit .env with your credentials:
|
| 489 |
# DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD
|
| 490 |
# GOOGLE_MAPS_API_KEY
|
|
|
|
|
|
|
| 491 |
|
| 492 |
-
#
|
| 493 |
-
python
|
| 494 |
-
|
| 495 |
-
# Run locally (stdio mode for Claude Desktop)
|
| 496 |
-
python server.py
|
| 497 |
|
| 498 |
# Run locally (SSE mode for web clients)
|
| 499 |
python app.py
|
| 500 |
```
|
| 501 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
### Testing
|
| 503 |
|
| 504 |
```bash
|
|
@@ -554,6 +628,53 @@ Upload files directly to HF Space:
|
|
| 554 |
|
| 555 |
---
|
| 556 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
## 📝 Environment Variables
|
| 558 |
|
| 559 |
Required:
|
|
@@ -567,6 +688,10 @@ DB_PASSWORD=your_password
|
|
| 567 |
|
| 568 |
# Google Maps API
|
| 569 |
GOOGLE_MAPS_API_KEY=your_api_key
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
```
|
| 571 |
|
| 572 |
Optional:
|
|
@@ -575,6 +700,10 @@ Optional:
|
|
| 575 |
PORT=7860
|
| 576 |
HOST=0.0.0.0
|
| 577 |
LOG_LEVEL=INFO
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
```
|
| 579 |
|
| 580 |
---
|
|
|
|
| 72 |
|
| 73 |
### Key Features
|
| 74 |
|
| 75 |
+
✅ **🔑 API Key Authentication** - Secure multi-tenant access with personal API keys
|
| 76 |
✅ **29 AI Tools** - Order, Driver & Assignment Management (including Gemini 2.0 Flash AI)
|
| 77 |
✅ **2 Real-Time Resources** - Live data feeds (orders://all, drivers://all)
|
| 78 |
✅ **Google Maps Integration** - Geocoding & Route Calculation with traffic data
|
|
|
|
| 156 |
|
| 157 |
### Connect from Claude Desktop
|
| 158 |
|
| 159 |
+
#### **Step 1: Get Your API Key** 🔑
|
| 160 |
|
| 161 |
+
Visit the FleetMind server and generate your personal API key:
|
| 162 |
+
|
| 163 |
+
👉 **https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/generate-key**
|
| 164 |
+
|
| 165 |
+
1. Enter your email address
|
| 166 |
+
2. Enter your name (optional)
|
| 167 |
+
3. Click "Generate API Key"
|
| 168 |
+
4. **Copy your API key immediately** - it won't be shown again!
|
| 169 |
+
|
| 170 |
+
Your API key will look like: `fm_xxxxxxxxxxxxxxxxxxxxxxxx`
|
| 171 |
+
|
| 172 |
+
#### **Step 2: Install Claude Desktop**
|
| 173 |
+
|
| 174 |
+
Download from: https://claude.ai/download
|
| 175 |
+
|
| 176 |
+
#### **Step 3: Configure MCP Server**
|
| 177 |
+
|
| 178 |
+
Edit your `claude_desktop_config.json`:
|
| 179 |
|
| 180 |
**For Production (HuggingFace Space):**
|
| 181 |
```json
|
| 182 |
{
|
| 183 |
"mcpServers": {
|
| 184 |
+
"fleetmind": {
|
| 185 |
"command": "npx",
|
| 186 |
"args": [
|
| 187 |
"mcp-remote",
|
| 188 |
"https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse"
|
| 189 |
+
],
|
| 190 |
+
"env": {
|
| 191 |
+
"FLEETMIND_API_KEY": "fm_your_api_key_here"
|
| 192 |
+
}
|
| 193 |
}
|
| 194 |
}
|
| 195 |
}
|
| 196 |
```
|
| 197 |
|
| 198 |
+
⚠️ **Important:** Replace `fm_your_api_key_here` with your actual API key from Step 1!
|
| 199 |
+
|
| 200 |
+
**For Local Development (with API Key):**
|
| 201 |
```json
|
| 202 |
{
|
| 203 |
"mcpServers": {
|
| 204 |
+
"fleetmind_local": {
|
| 205 |
+
"command": "npx",
|
| 206 |
+
"args": [
|
| 207 |
+
"mcp-remote",
|
| 208 |
+
"http://localhost:7860/sse"
|
| 209 |
+
],
|
| 210 |
+
"env": {
|
| 211 |
+
"FLEETMIND_API_KEY": "your_local_api_key"
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
}
|
| 216 |
+
```
|
| 217 |
+
|
| 218 |
+
**For Local Development (SKIP_AUTH mode):**
|
| 219 |
+
```json
|
| 220 |
+
{
|
| 221 |
+
"mcpServers": {
|
| 222 |
+
"fleetmind_local": {
|
| 223 |
"command": "npx",
|
| 224 |
"args": [
|
| 225 |
"mcp-remote",
|
|
|
|
| 229 |
}
|
| 230 |
}
|
| 231 |
```
|
| 232 |
+
*Set `SKIP_AUTH=true` in your `.env` file for local testing without API keys*
|
| 233 |
|
| 234 |
**Both (Production + Local):**
|
| 235 |
```json
|
| 236 |
{
|
| 237 |
"mcpServers": {
|
| 238 |
+
"fleetmind_local": {
|
| 239 |
"command": "npx",
|
| 240 |
+
"args": ["mcp-remote", "http://localhost:7860/sse"],
|
| 241 |
+
"env": {
|
| 242 |
+
"FLEETMIND_API_KEY": "your_local_key"
|
| 243 |
+
}
|
| 244 |
},
|
| 245 |
+
"fleetmind": {
|
| 246 |
"command": "npx",
|
| 247 |
+
"args": ["mcp-remote", "https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse"],
|
| 248 |
+
"env": {
|
| 249 |
+
"FLEETMIND_API_KEY": "your_production_key"
|
| 250 |
+
}
|
| 251 |
}
|
| 252 |
}
|
| 253 |
}
|
| 254 |
```
|
| 255 |
|
| 256 |
+
#### **Step 4: Restart Claude Desktop**
|
| 257 |
+
|
| 258 |
+
Restart Claude Desktop - FleetMind tools will appear automatically!
|
| 259 |
|
| 260 |
+
#### **Step 5: Try It Out!**
|
| 261 |
- "Create an urgent delivery order for Sarah at 456 Oak Ave, San Francisco"
|
| 262 |
- "Use intelligent AI assignment to find the best driver for this order"
|
| 263 |
- "Show me all available drivers"
|
|
|
|
| 522 |
- Python 3.10+
|
| 523 |
- PostgreSQL database (or use Neon serverless)
|
| 524 |
- Google Maps API key
|
| 525 |
+
- Google Gemini API key (for AI features)
|
| 526 |
|
| 527 |
### Setup
|
| 528 |
|
|
|
|
| 539 |
# Edit .env with your credentials:
|
| 540 |
# DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD
|
| 541 |
# GOOGLE_MAPS_API_KEY
|
| 542 |
+
# GOOGLE_API_KEY (for Gemini)
|
| 543 |
+
# SKIP_AUTH=true # For local development without API keys
|
| 544 |
|
| 545 |
+
# Run database migrations
|
| 546 |
+
python database/migrations/001_initial_schema.py
|
| 547 |
+
# ... run all migrations
|
|
|
|
|
|
|
| 548 |
|
| 549 |
# Run locally (SSE mode for web clients)
|
| 550 |
python app.py
|
| 551 |
```
|
| 552 |
|
| 553 |
+
### API Key Management (Local)
|
| 554 |
+
|
| 555 |
+
**Generate API Keys:**
|
| 556 |
+
```bash
|
| 557 |
+
# Interactive mode
|
| 558 |
+
python generate_api_key.py
|
| 559 |
+
|
| 560 |
+
# With arguments
|
| 561 |
+
python generate_api_key.py --email user@example.com --name "Test User"
|
| 562 |
+
|
| 563 |
+
# List all keys
|
| 564 |
+
python generate_api_key.py --list
|
| 565 |
+
|
| 566 |
+
# Revoke a key
|
| 567 |
+
python generate_api_key.py --revoke user@example.com
|
| 568 |
+
```
|
| 569 |
+
|
| 570 |
+
**Web Interface:**
|
| 571 |
+
Visit http://localhost:7860/generate-key to generate keys via web form.
|
| 572 |
+
|
| 573 |
+
**Development Mode (No API Keys):**
|
| 574 |
+
Set `SKIP_AUTH=true` in `.env` to bypass authentication for local testing.
|
| 575 |
+
|
| 576 |
### Testing
|
| 577 |
|
| 578 |
```bash
|
|
|
|
| 628 |
|
| 629 |
---
|
| 630 |
|
| 631 |
+
## 🔒 Authentication & Security
|
| 632 |
+
|
| 633 |
+
FleetMind uses **API Key authentication** for secure multi-tenant access:
|
| 634 |
+
|
| 635 |
+
### How It Works
|
| 636 |
+
|
| 637 |
+
1. **User generates API key** via web interface (`/generate-key`)
|
| 638 |
+
2. **API key is hashed** (SHA-256) before storage - never stored in plaintext
|
| 639 |
+
3. **User adds key** to Claude Desktop config in `env.FLEETMIND_API_KEY`
|
| 640 |
+
4. **Server validates key** on each request
|
| 641 |
+
5. **Data is isolated** - each user only sees their own orders/drivers
|
| 642 |
+
|
| 643 |
+
### Security Features
|
| 644 |
+
|
| 645 |
+
- ✅ **One-Time Display** - API keys shown only once during generation
|
| 646 |
+
- ✅ **Hashed Storage** - SHA-256 hashing, never stored in plaintext
|
| 647 |
+
- ✅ **Multi-Tenant** - Complete data isolation per user
|
| 648 |
+
- ✅ **Easy Revocation** - Keys can be revoked via CLI or web interface
|
| 649 |
+
- ✅ **Development Mode** - `SKIP_AUTH=true` for local testing
|
| 650 |
+
|
| 651 |
+
### API Key Format
|
| 652 |
+
|
| 653 |
+
```
|
| 654 |
+
fm_LAISKBMKmQxoapDbLIhr_KqbVL69AS58wBtdpYfbQ10
|
| 655 |
+
```
|
| 656 |
+
|
| 657 |
+
- Prefix: `fm_` (FleetMind)
|
| 658 |
+
- 43 random characters (URL-safe base64)
|
| 659 |
+
- Unique per user
|
| 660 |
+
|
| 661 |
+
### Managing Keys
|
| 662 |
+
|
| 663 |
+
```bash
|
| 664 |
+
# Generate new key
|
| 665 |
+
python generate_api_key.py --email user@example.com
|
| 666 |
+
|
| 667 |
+
# List all keys
|
| 668 |
+
python generate_api_key.py --list
|
| 669 |
+
|
| 670 |
+
# Revoke a key
|
| 671 |
+
python generate_api_key.py --revoke user@example.com
|
| 672 |
+
```
|
| 673 |
+
|
| 674 |
+
Or use the web interface: `http://localhost:7860/generate-key`
|
| 675 |
+
|
| 676 |
+
---
|
| 677 |
+
|
| 678 |
## 📝 Environment Variables
|
| 679 |
|
| 680 |
Required:
|
|
|
|
| 688 |
|
| 689 |
# Google Maps API
|
| 690 |
GOOGLE_MAPS_API_KEY=your_api_key
|
| 691 |
+
|
| 692 |
+
# Google Gemini API
|
| 693 |
+
GOOGLE_API_KEY=your_gemini_key
|
| 694 |
+
AI_PROVIDER=gemini
|
| 695 |
```
|
| 696 |
|
| 697 |
Optional:
|
|
|
|
| 700 |
PORT=7860
|
| 701 |
HOST=0.0.0.0
|
| 702 |
LOG_LEVEL=INFO
|
| 703 |
+
SERVER_URL=http://localhost:7860
|
| 704 |
+
|
| 705 |
+
# Authentication
|
| 706 |
+
SKIP_AUTH=true # Set to false for production
|
| 707 |
```
|
| 708 |
|
| 709 |
---
|
app.py
CHANGED
|
@@ -94,25 +94,9 @@ if __name__ == "__main__":
|
|
| 94 |
logger.info("=" * 70)
|
| 95 |
|
| 96 |
try:
|
| 97 |
-
# Add landing page and
|
| 98 |
from starlette.responses import HTMLResponse, JSONResponse
|
| 99 |
-
|
| 100 |
-
@mcp.custom_route("/.well-known/oauth-protected-resource", methods=["GET"])
|
| 101 |
-
async def oauth_metadata(request):
|
| 102 |
-
"""OAuth 2.0 Protected Resource Metadata (RFC 9728)"""
|
| 103 |
-
return JSONResponse({
|
| 104 |
-
"resource": os.getenv('SERVER_URL', 'http://localhost:7860'),
|
| 105 |
-
"authorization_servers": [
|
| 106 |
-
"https://test.stytch.com/v1/public"
|
| 107 |
-
],
|
| 108 |
-
"scopes_supported": [
|
| 109 |
-
"orders:read", "orders:write",
|
| 110 |
-
"drivers:read", "drivers:write",
|
| 111 |
-
"assignments:manage", "admin"
|
| 112 |
-
],
|
| 113 |
-
"bearer_methods_supported": ["header"],
|
| 114 |
-
"resource_signing_alg_values_supported": ["RS256"]
|
| 115 |
-
})
|
| 116 |
|
| 117 |
@mcp.custom_route("/", methods=["GET"])
|
| 118 |
async def landing_page(request):
|
|
@@ -155,7 +139,15 @@ if __name__ == "__main__":
|
|
| 155 |
<code>https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse</code>
|
| 156 |
</div>
|
| 157 |
|
| 158 |
-
<h3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
<p>Add this to your <code>claude_desktop_config.json</code> file:</p>
|
| 160 |
<pre>{
|
| 161 |
"mcpServers": {
|
|
@@ -164,16 +156,20 @@ if __name__ == "__main__":
|
|
| 164 |
"args": [
|
| 165 |
"mcp-remote",
|
| 166 |
"https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse"
|
| 167 |
-
]
|
|
|
|
|
|
|
|
|
|
| 168 |
}
|
| 169 |
}
|
| 170 |
}</pre>
|
| 171 |
|
| 172 |
-
<h3>📋
|
| 173 |
<ol>
|
|
|
|
| 174 |
<li>Install <a href="https://claude.ai/download" target="_blank">Claude Desktop</a></li>
|
| 175 |
<li>Locate your <code>claude_desktop_config.json</code> file</li>
|
| 176 |
-
<li>Add the configuration
|
| 177 |
<li>Restart Claude Desktop</li>
|
| 178 |
<li>Look for "FleetMind" in the 🔌 tools menu</li>
|
| 179 |
</ol>
|
|
@@ -211,6 +207,7 @@ if __name__ == "__main__":
|
|
| 211 |
|
| 212 |
<h2>⭐ Key Features</h2>
|
| 213 |
<ul>
|
|
|
|
| 214 |
<li><strong>🧠 Gemini 2.0 Flash AI</strong> - Intelligent order assignment with detailed reasoning</li>
|
| 215 |
<li><strong>🌦️ Weather-Aware Routing</strong> - Safety-first delivery planning with OpenWeatherMap</li>
|
| 216 |
<li><strong>🚦 Real-Time Traffic</strong> - Google Routes API integration with live traffic data</li>
|
|
@@ -239,7 +236,149 @@ if __name__ == "__main__":
|
|
| 239 |
</html>
|
| 240 |
""")
|
| 241 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
logger.info("[OK] Landing page added at / route")
|
|
|
|
| 243 |
logger.info("[OK] MCP SSE endpoint available at /sse")
|
| 244 |
|
| 245 |
# Run MCP server with SSE transport (includes both /sse and custom routes)
|
|
|
|
| 94 |
logger.info("=" * 70)
|
| 95 |
|
| 96 |
try:
|
| 97 |
+
# Add web routes for landing page and API key generation
|
| 98 |
from starlette.responses import HTMLResponse, JSONResponse
|
| 99 |
+
from database.api_keys import generate_api_key as db_generate_api_key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
@mcp.custom_route("/", methods=["GET"])
|
| 102 |
async def landing_page(request):
|
|
|
|
| 139 |
<code>https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse</code>
|
| 140 |
</div>
|
| 141 |
|
| 142 |
+
<h3>🔑 Step 1: Get Your API Key</h3>
|
| 143 |
+
<p style="text-align: center; margin: 20px 0;">
|
| 144 |
+
<a href="/generate-key" style="display: inline-block; background: #3b82f6; color: white; padding: 15px 30px; border-radius: 8px; text-decoration: none; font-weight: bold; font-size: 18px;">
|
| 145 |
+
Generate API Key →
|
| 146 |
+
</a>
|
| 147 |
+
</p>
|
| 148 |
+
<p>Click the button above to generate your unique API key. You'll need this to authenticate with the server.</p>
|
| 149 |
+
|
| 150 |
+
<h3>⚙️ Step 2: Configure Claude Desktop</h3>
|
| 151 |
<p>Add this to your <code>claude_desktop_config.json</code> file:</p>
|
| 152 |
<pre>{
|
| 153 |
"mcpServers": {
|
|
|
|
| 156 |
"args": [
|
| 157 |
"mcp-remote",
|
| 158 |
"https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse"
|
| 159 |
+
],
|
| 160 |
+
"env": {
|
| 161 |
+
"FLEETMIND_API_KEY": "fm_your_api_key_here"
|
| 162 |
+
}
|
| 163 |
}
|
| 164 |
}
|
| 165 |
}</pre>
|
| 166 |
|
| 167 |
+
<h3>📋 Step 3: Connect</h3>
|
| 168 |
<ol>
|
| 169 |
+
<li><strong>Generate your API key</strong> using the button above</li>
|
| 170 |
<li>Install <a href="https://claude.ai/download" target="_blank">Claude Desktop</a></li>
|
| 171 |
<li>Locate your <code>claude_desktop_config.json</code> file</li>
|
| 172 |
+
<li>Add the configuration (replace <code>fm_your_api_key_here</code> with your actual key)</li>
|
| 173 |
<li>Restart Claude Desktop</li>
|
| 174 |
<li>Look for "FleetMind" in the 🔌 tools menu</li>
|
| 175 |
</ol>
|
|
|
|
| 207 |
|
| 208 |
<h2>⭐ Key Features</h2>
|
| 209 |
<ul>
|
| 210 |
+
<li><strong>🔑 API Key Authentication</strong> - Secure multi-tenant access with personal API keys</li>
|
| 211 |
<li><strong>🧠 Gemini 2.0 Flash AI</strong> - Intelligent order assignment with detailed reasoning</li>
|
| 212 |
<li><strong>🌦️ Weather-Aware Routing</strong> - Safety-first delivery planning with OpenWeatherMap</li>
|
| 213 |
<li><strong>🚦 Real-Time Traffic</strong> - Google Routes API integration with live traffic data</li>
|
|
|
|
| 236 |
</html>
|
| 237 |
""")
|
| 238 |
|
| 239 |
+
@mcp.custom_route("/generate-key", methods=["GET", "POST"])
|
| 240 |
+
async def generate_key_page(request):
|
| 241 |
+
"""API Key generation page"""
|
| 242 |
+
if request.method == "GET":
|
| 243 |
+
return HTMLResponse("""
|
| 244 |
+
<!DOCTYPE html>
|
| 245 |
+
<html>
|
| 246 |
+
<head>
|
| 247 |
+
<title>Generate FleetMind API Key</title>
|
| 248 |
+
<style>
|
| 249 |
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
|
| 250 |
+
.container { background: #1e293b; padding: 40px; border-radius: 12px; box-shadow: 0 8px 16px rgba(0,0,0,0.4); }
|
| 251 |
+
h1 { color: #f1f5f9; }
|
| 252 |
+
input { width: 100%; padding: 12px; margin: 10px 0; border-radius: 6px; border: 1px solid #334155; background: #0f172a; color: #e2e8f0; font-size: 16px; }
|
| 253 |
+
button { background: #3b82f6; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; cursor: pointer; width: 100%; }
|
| 254 |
+
button:hover { background: #2563eb; }
|
| 255 |
+
.info { background: #1e3a5f; padding: 15px; border-radius: 6px; margin: 15px 0; border-left: 4px solid #3b82f6; }
|
| 256 |
+
</style>
|
| 257 |
+
</head>
|
| 258 |
+
<body>
|
| 259 |
+
<div class="container">
|
| 260 |
+
<h1>🔑 Generate API Key</h1>
|
| 261 |
+
<p>Create your FleetMind MCP Server API key</p>
|
| 262 |
+
|
| 263 |
+
<div class="info">
|
| 264 |
+
<strong>📋 What you'll need:</strong><br>
|
| 265 |
+
• Your email address<br>
|
| 266 |
+
• Your name (optional)<br>
|
| 267 |
+
<br>
|
| 268 |
+
After generation, you'll get an API key to use with Claude Desktop.
|
| 269 |
+
</div>
|
| 270 |
+
|
| 271 |
+
<form method="POST">
|
| 272 |
+
<input type="email" name="email" placeholder="Your email address" required>
|
| 273 |
+
<input type="text" name="name" placeholder="Your name (optional)">
|
| 274 |
+
<button type="submit">Generate API Key</button>
|
| 275 |
+
</form>
|
| 276 |
+
|
| 277 |
+
<p style="text-align: center; margin-top: 20px;">
|
| 278 |
+
<a href="/" style="color: #60a5fa; text-decoration: none;">← Back to Home</a>
|
| 279 |
+
</p>
|
| 280 |
+
</div>
|
| 281 |
+
</body>
|
| 282 |
+
</html>
|
| 283 |
+
""")
|
| 284 |
+
|
| 285 |
+
# POST - Generate API key
|
| 286 |
+
else:
|
| 287 |
+
try:
|
| 288 |
+
form_data = await request.form()
|
| 289 |
+
email = form_data.get("email")
|
| 290 |
+
name = form_data.get("name") or None
|
| 291 |
+
|
| 292 |
+
if not email:
|
| 293 |
+
return HTMLResponse("<h1>Error: Email is required</h1>", status_code=400)
|
| 294 |
+
|
| 295 |
+
result = db_generate_api_key(email, name)
|
| 296 |
+
|
| 297 |
+
if not result['success']:
|
| 298 |
+
return HTMLResponse(f"<h1>Error: {result['error']}</h1>", status_code=400)
|
| 299 |
+
|
| 300 |
+
# Success - show the API key (one time only!)
|
| 301 |
+
return HTMLResponse(f"""
|
| 302 |
+
<!DOCTYPE html>
|
| 303 |
+
<html>
|
| 304 |
+
<head>
|
| 305 |
+
<title>Your FleetMind API Key</title>
|
| 306 |
+
<style>
|
| 307 |
+
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }}
|
| 308 |
+
.container {{ background: #1e293b; padding: 40px; border-radius: 12px; box-shadow: 0 8px 16px rgba(0,0,0,0.4); }}
|
| 309 |
+
h1 {{ color: #f1f5f9; }}
|
| 310 |
+
.success {{ background: #10b981; padding: 15px; border-radius: 6px; margin: 15px 0; }}
|
| 311 |
+
.warning {{ background: #ef4444; padding: 15px; border-radius: 6px; margin: 15px 0; }}
|
| 312 |
+
code {{ background: #334155; color: #60a5fa; padding: 3px 8px; border-radius: 4px; font-family: 'Courier New', monospace; display: block; margin: 10px 0; word-wrap: break-word; font-size: 14px; }}
|
| 313 |
+
pre {{ background: #0f172a; color: #f1f5f9; padding: 20px; border-radius: 8px; overflow-x: auto; border: 1px solid #334155; }}
|
| 314 |
+
button {{ background: #3b82f6; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; cursor: pointer; margin: 10px 5px; }}
|
| 315 |
+
button:hover {{ background: #2563eb; }}
|
| 316 |
+
</style>
|
| 317 |
+
<script>
|
| 318 |
+
function copyKey() {{
|
| 319 |
+
navigator.clipboard.writeText('{result["api_key"]}');
|
| 320 |
+
alert('API key copied to clipboard!');
|
| 321 |
+
}}
|
| 322 |
+
</script>
|
| 323 |
+
</head>
|
| 324 |
+
<body>
|
| 325 |
+
<div class="container">
|
| 326 |
+
<h1>✅ API Key Generated!</h1>
|
| 327 |
+
|
| 328 |
+
<div class="success">
|
| 329 |
+
<strong>Your API key has been created successfully</strong>
|
| 330 |
+
</div>
|
| 331 |
+
|
| 332 |
+
<p><strong>Email:</strong> {result["email"]}</p>
|
| 333 |
+
<p><strong>Name:</strong> {result["name"]}</p>
|
| 334 |
+
<p><strong>User ID:</strong> {result["user_id"]}</p>
|
| 335 |
+
|
| 336 |
+
<div class="warning">
|
| 337 |
+
<strong>⚠️ SAVE THIS KEY NOW - IT WON'T BE SHOWN AGAIN!</strong>
|
| 338 |
+
</div>
|
| 339 |
+
|
| 340 |
+
<h2>🔑 Your API Key:</h2>
|
| 341 |
+
<code>{result["api_key"]}</code>
|
| 342 |
+
<button onclick="copyKey()">📋 Copy Key</button>
|
| 343 |
+
|
| 344 |
+
<h2>📋 Claude Desktop Setup:</h2>
|
| 345 |
+
<p>Add this to your <code>claude_desktop_config.json</code>:</p>
|
| 346 |
+
<pre>{{
|
| 347 |
+
"mcpServers": {{
|
| 348 |
+
"fleetmind": {{
|
| 349 |
+
"command": "npx",
|
| 350 |
+
"args": [
|
| 351 |
+
"mcp-remote",
|
| 352 |
+
"https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse"
|
| 353 |
+
],
|
| 354 |
+
"env": {{
|
| 355 |
+
"FLEETMIND_API_KEY": "{result["api_key"]}"
|
| 356 |
+
}}
|
| 357 |
+
}}
|
| 358 |
+
}}
|
| 359 |
+
}}</pre>
|
| 360 |
+
|
| 361 |
+
<h2>🚀 Next Steps:</h2>
|
| 362 |
+
<ol>
|
| 363 |
+
<li>Copy your API key (click the button above)</li>
|
| 364 |
+
<li>Add it to your Claude Desktop config</li>
|
| 365 |
+
<li>Restart Claude Desktop</li>
|
| 366 |
+
<li>Start using FleetMind tools!</li>
|
| 367 |
+
</ol>
|
| 368 |
+
|
| 369 |
+
<p style="text-align: center; margin-top: 30px;">
|
| 370 |
+
<a href="/" style="color: #60a5fa; text-decoration: none;">← Back to Home</a>
|
| 371 |
+
</p>
|
| 372 |
+
</div>
|
| 373 |
+
</body>
|
| 374 |
+
</html>
|
| 375 |
+
""")
|
| 376 |
+
|
| 377 |
+
except Exception as e:
|
| 378 |
+
return HTMLResponse(f"<h1>Error: {str(e)}</h1>", status_code=500)
|
| 379 |
+
|
| 380 |
logger.info("[OK] Landing page added at / route")
|
| 381 |
+
logger.info("[OK] API key generation page added at /generate-key")
|
| 382 |
logger.info("[OK] MCP SSE endpoint available at /sse")
|
| 383 |
|
| 384 |
# Run MCP server with SSE transport (includes both /sse and custom routes)
|
database/api_keys.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
API Key Authentication System for FleetMind MCP Server
|
| 3 |
+
|
| 4 |
+
Simple API key management for multi-tenant authentication without OAuth complexity.
|
| 5 |
+
Works with Claude Desktop and mcp-remote today!
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
1. User generates API key via web interface or CLI
|
| 9 |
+
2. User adds API key to Claude Desktop config
|
| 10 |
+
3. MCP server validates key and returns user_id
|
| 11 |
+
4. Multi-tenant isolation works automatically
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import secrets
|
| 16 |
+
import hashlib
|
| 17 |
+
from datetime import datetime
|
| 18 |
+
from typing import Optional, Dict
|
| 19 |
+
from database.connection import get_db_connection
|
| 20 |
+
|
| 21 |
+
def create_api_keys_table():
|
| 22 |
+
"""Create api_keys table if it doesn't exist"""
|
| 23 |
+
conn = get_db_connection()
|
| 24 |
+
cursor = conn.cursor()
|
| 25 |
+
|
| 26 |
+
cursor.execute("""
|
| 27 |
+
CREATE TABLE IF NOT EXISTS api_keys (
|
| 28 |
+
key_id SERIAL PRIMARY KEY,
|
| 29 |
+
user_id VARCHAR(100) NOT NULL,
|
| 30 |
+
email VARCHAR(255) NOT NULL,
|
| 31 |
+
name VARCHAR(255),
|
| 32 |
+
api_key_hash VARCHAR(64) NOT NULL UNIQUE,
|
| 33 |
+
api_key_prefix VARCHAR(20) NOT NULL,
|
| 34 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 35 |
+
last_used_at TIMESTAMP,
|
| 36 |
+
is_active BOOLEAN DEFAULT true,
|
| 37 |
+
UNIQUE(user_id)
|
| 38 |
+
)
|
| 39 |
+
""")
|
| 40 |
+
|
| 41 |
+
cursor.execute("""
|
| 42 |
+
CREATE INDEX IF NOT EXISTS idx_api_keys_hash
|
| 43 |
+
ON api_keys(api_key_hash)
|
| 44 |
+
""")
|
| 45 |
+
|
| 46 |
+
cursor.execute("""
|
| 47 |
+
CREATE INDEX IF NOT EXISTS idx_api_keys_user
|
| 48 |
+
ON api_keys(user_id)
|
| 49 |
+
""")
|
| 50 |
+
|
| 51 |
+
conn.commit()
|
| 52 |
+
cursor.close()
|
| 53 |
+
conn.close()
|
| 54 |
+
|
| 55 |
+
def generate_api_key(email: str, name: str = None) -> Dict[str, str]:
|
| 56 |
+
"""
|
| 57 |
+
Generate a new API key for a user
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
email: User's email address (used as identifier)
|
| 61 |
+
name: User's display name (optional)
|
| 62 |
+
|
| 63 |
+
Returns:
|
| 64 |
+
dict with api_key (show once!), user_id, email, name
|
| 65 |
+
"""
|
| 66 |
+
# Generate secure random API key
|
| 67 |
+
api_key = f"fm_{secrets.token_urlsafe(32)}" # fm_ prefix for FleetMind
|
| 68 |
+
|
| 69 |
+
# Hash the API key for storage (never store plain text!)
|
| 70 |
+
api_key_hash = hashlib.sha256(api_key.encode()).hexdigest()
|
| 71 |
+
|
| 72 |
+
# Store prefix for display (first 12 chars)
|
| 73 |
+
api_key_prefix = api_key[:12]
|
| 74 |
+
|
| 75 |
+
# Generate user_id from email
|
| 76 |
+
user_id = f"user_{hashlib.md5(email.encode()).hexdigest()[:12]}"
|
| 77 |
+
|
| 78 |
+
conn = get_db_connection()
|
| 79 |
+
cursor = conn.cursor()
|
| 80 |
+
|
| 81 |
+
try:
|
| 82 |
+
# Check if user already has a key
|
| 83 |
+
cursor.execute("SELECT user_id FROM api_keys WHERE email = %s", (email,))
|
| 84 |
+
existing = cursor.fetchone()
|
| 85 |
+
|
| 86 |
+
if existing:
|
| 87 |
+
cursor.close()
|
| 88 |
+
conn.close()
|
| 89 |
+
return {
|
| 90 |
+
"success": False,
|
| 91 |
+
"error": "User already has an API key. Revoke the old key first."
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
# Insert new API key
|
| 95 |
+
cursor.execute("""
|
| 96 |
+
INSERT INTO api_keys (user_id, email, name, api_key_hash, api_key_prefix)
|
| 97 |
+
VALUES (%s, %s, %s, %s, %s)
|
| 98 |
+
RETURNING user_id, email, name, created_at
|
| 99 |
+
""", (user_id, email, name, api_key_hash, api_key_prefix))
|
| 100 |
+
|
| 101 |
+
result = cursor.fetchone()
|
| 102 |
+
conn.commit()
|
| 103 |
+
|
| 104 |
+
if not result:
|
| 105 |
+
raise Exception("Failed to insert API key")
|
| 106 |
+
|
| 107 |
+
# Unpack the result tuple
|
| 108 |
+
ret_user_id, ret_email, ret_name, ret_created_at = result
|
| 109 |
+
|
| 110 |
+
return {
|
| 111 |
+
"success": True,
|
| 112 |
+
"api_key": api_key, # SHOW THIS ONCE! Never displayed again
|
| 113 |
+
"user_id": ret_user_id,
|
| 114 |
+
"email": ret_email,
|
| 115 |
+
"name": ret_name or "FleetMind User",
|
| 116 |
+
"created_at": str(ret_created_at) if ret_created_at else "",
|
| 117 |
+
"message": "⚠️ IMPORTANT: Save this API key now! It won't be shown again."
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
except Exception as e:
|
| 121 |
+
conn.rollback()
|
| 122 |
+
import traceback
|
| 123 |
+
error_details = traceback.format_exc()
|
| 124 |
+
print(f"API Key Generation Error: {e}")
|
| 125 |
+
print(f"Error details: {error_details}")
|
| 126 |
+
return {
|
| 127 |
+
"success": False,
|
| 128 |
+
"error": f"Failed to generate API key: {str(e)}"
|
| 129 |
+
}
|
| 130 |
+
finally:
|
| 131 |
+
cursor.close()
|
| 132 |
+
conn.close()
|
| 133 |
+
|
| 134 |
+
def verify_api_key(api_key: str) -> Optional[Dict[str, str]]:
|
| 135 |
+
"""
|
| 136 |
+
Verify API key and return user info
|
| 137 |
+
|
| 138 |
+
Args:
|
| 139 |
+
api_key: The API key to verify
|
| 140 |
+
|
| 141 |
+
Returns:
|
| 142 |
+
User info dict if valid, None if invalid
|
| 143 |
+
"""
|
| 144 |
+
if not api_key or not api_key.startswith("fm_"):
|
| 145 |
+
return None
|
| 146 |
+
|
| 147 |
+
# Hash the provided key
|
| 148 |
+
api_key_hash = hashlib.sha256(api_key.encode()).hexdigest()
|
| 149 |
+
|
| 150 |
+
conn = get_db_connection()
|
| 151 |
+
cursor = conn.cursor()
|
| 152 |
+
|
| 153 |
+
try:
|
| 154 |
+
# Look up the key
|
| 155 |
+
cursor.execute("""
|
| 156 |
+
SELECT user_id, email, name, is_active
|
| 157 |
+
FROM api_keys
|
| 158 |
+
WHERE api_key_hash = %s
|
| 159 |
+
""", (api_key_hash,))
|
| 160 |
+
|
| 161 |
+
result = cursor.fetchone()
|
| 162 |
+
|
| 163 |
+
if not result:
|
| 164 |
+
return None
|
| 165 |
+
|
| 166 |
+
user_id, email, name, is_active = result
|
| 167 |
+
|
| 168 |
+
if not is_active:
|
| 169 |
+
return None
|
| 170 |
+
|
| 171 |
+
# Update last_used_at
|
| 172 |
+
cursor.execute("""
|
| 173 |
+
UPDATE api_keys
|
| 174 |
+
SET last_used_at = CURRENT_TIMESTAMP
|
| 175 |
+
WHERE api_key_hash = %s
|
| 176 |
+
""", (api_key_hash,))
|
| 177 |
+
conn.commit()
|
| 178 |
+
|
| 179 |
+
return {
|
| 180 |
+
'user_id': user_id,
|
| 181 |
+
'email': email,
|
| 182 |
+
'name': name or 'FleetMind User',
|
| 183 |
+
'scopes': ['orders:read', 'orders:write', 'drivers:read', 'drivers:write', 'assignments:manage']
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
except Exception as e:
|
| 187 |
+
print(f"API key verification error: {e}")
|
| 188 |
+
return None
|
| 189 |
+
finally:
|
| 190 |
+
cursor.close()
|
| 191 |
+
conn.close()
|
| 192 |
+
|
| 193 |
+
def list_api_keys() -> list:
|
| 194 |
+
"""List all API keys (without showing actual keys)"""
|
| 195 |
+
conn = get_db_connection()
|
| 196 |
+
cursor = conn.cursor()
|
| 197 |
+
|
| 198 |
+
cursor.execute("""
|
| 199 |
+
SELECT user_id, email, name, api_key_prefix, created_at, last_used_at, is_active
|
| 200 |
+
FROM api_keys
|
| 201 |
+
ORDER BY created_at DESC
|
| 202 |
+
""")
|
| 203 |
+
|
| 204 |
+
keys = []
|
| 205 |
+
for row in cursor.fetchall():
|
| 206 |
+
keys.append({
|
| 207 |
+
'user_id': row[0],
|
| 208 |
+
'email': row[1],
|
| 209 |
+
'name': row[2],
|
| 210 |
+
'key_preview': f"{row[3]}...",
|
| 211 |
+
'created_at': row[4].isoformat(),
|
| 212 |
+
'last_used_at': row[5].isoformat() if row[5] else None,
|
| 213 |
+
'is_active': row[6]
|
| 214 |
+
})
|
| 215 |
+
|
| 216 |
+
cursor.close()
|
| 217 |
+
conn.close()
|
| 218 |
+
return keys
|
| 219 |
+
|
| 220 |
+
def revoke_api_key(email: str) -> Dict[str, any]:
|
| 221 |
+
"""Revoke (deactivate) an API key"""
|
| 222 |
+
conn = get_db_connection()
|
| 223 |
+
cursor = conn.cursor()
|
| 224 |
+
|
| 225 |
+
try:
|
| 226 |
+
cursor.execute("""
|
| 227 |
+
UPDATE api_keys
|
| 228 |
+
SET is_active = false
|
| 229 |
+
WHERE email = %s
|
| 230 |
+
RETURNING user_id, email
|
| 231 |
+
""", (email,))
|
| 232 |
+
|
| 233 |
+
result = cursor.fetchone()
|
| 234 |
+
conn.commit()
|
| 235 |
+
|
| 236 |
+
if result:
|
| 237 |
+
return {
|
| 238 |
+
"success": True,
|
| 239 |
+
"message": f"API key revoked for {result[1]}"
|
| 240 |
+
}
|
| 241 |
+
else:
|
| 242 |
+
return {
|
| 243 |
+
"success": False,
|
| 244 |
+
"error": "No API key found for this email"
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
except Exception as e:
|
| 248 |
+
conn.rollback()
|
| 249 |
+
return {
|
| 250 |
+
"success": False,
|
| 251 |
+
"error": f"Failed to revoke key: {str(e)}"
|
| 252 |
+
}
|
| 253 |
+
finally:
|
| 254 |
+
cursor.close()
|
| 255 |
+
conn.close()
|
| 256 |
+
|
| 257 |
+
# Initialize table on import
|
| 258 |
+
try:
|
| 259 |
+
create_api_keys_table()
|
| 260 |
+
except:
|
| 261 |
+
pass # Table might already exist
|
generate_api_key.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
FleetMind API Key Generator
|
| 4 |
+
|
| 5 |
+
Generate API keys for users to authenticate with FleetMind MCP Server.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python generate_api_key.py
|
| 9 |
+
python generate_api_key.py --email user@example.com --name "John Doe"
|
| 10 |
+
python generate_api_key.py --list
|
| 11 |
+
python generate_api_key.py --revoke user@example.com
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
import sys
|
| 16 |
+
from database.api_keys import generate_api_key, list_api_keys, revoke_api_key
|
| 17 |
+
|
| 18 |
+
def main():
|
| 19 |
+
parser = argparse.ArgumentParser(
|
| 20 |
+
description='FleetMind API Key Management',
|
| 21 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 22 |
+
epilog="""
|
| 23 |
+
Examples:
|
| 24 |
+
# Interactive mode (prompts for email and name)
|
| 25 |
+
python generate_api_key.py
|
| 26 |
+
|
| 27 |
+
# Generate key with arguments
|
| 28 |
+
python generate_api_key.py --email alice@company.com --name "Alice Smith"
|
| 29 |
+
|
| 30 |
+
# List all API keys
|
| 31 |
+
python generate_api_key.py --list
|
| 32 |
+
|
| 33 |
+
# Revoke a key
|
| 34 |
+
python generate_api_key.py --revoke alice@company.com
|
| 35 |
+
"""
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
parser.add_argument('--email', help='User email address')
|
| 39 |
+
parser.add_argument('--name', help='User display name')
|
| 40 |
+
parser.add_argument('--list', action='store_true', help='List all API keys')
|
| 41 |
+
parser.add_argument('--revoke', metavar='EMAIL', help='Revoke API key for email')
|
| 42 |
+
|
| 43 |
+
args = parser.parse_args()
|
| 44 |
+
|
| 45 |
+
# List API keys
|
| 46 |
+
if args.list:
|
| 47 |
+
print("\n" + "="*80)
|
| 48 |
+
print("FLEETMIND API KEYS")
|
| 49 |
+
print("="*80 + "\n")
|
| 50 |
+
|
| 51 |
+
keys = list_api_keys()
|
| 52 |
+
if not keys:
|
| 53 |
+
print("No API keys found.\n")
|
| 54 |
+
return
|
| 55 |
+
|
| 56 |
+
for key in keys:
|
| 57 |
+
status = "✅ Active" if key['is_active'] else "❌ Revoked"
|
| 58 |
+
print(f"Email: {key['email']}")
|
| 59 |
+
print(f"Name: {key['name']}")
|
| 60 |
+
print(f"User ID: {key['user_id']}")
|
| 61 |
+
print(f"Key: {key['key_preview']}")
|
| 62 |
+
print(f"Created: {key['created_at']}")
|
| 63 |
+
print(f"Last Used: {key['last_used_at'] or 'Never'}")
|
| 64 |
+
print(f"Status: {status}")
|
| 65 |
+
print("-" * 80)
|
| 66 |
+
|
| 67 |
+
print()
|
| 68 |
+
return
|
| 69 |
+
|
| 70 |
+
# Revoke API key
|
| 71 |
+
if args.revoke:
|
| 72 |
+
print(f"\n⚠️ Revoking API key for {args.revoke}...")
|
| 73 |
+
result = revoke_api_key(args.revoke)
|
| 74 |
+
|
| 75 |
+
if result['success']:
|
| 76 |
+
print(f"✅ {result['message']}\n")
|
| 77 |
+
else:
|
| 78 |
+
print(f"❌ Error: {result['error']}\n")
|
| 79 |
+
sys.exit(1)
|
| 80 |
+
return
|
| 81 |
+
|
| 82 |
+
# Generate new API key
|
| 83 |
+
if not args.email:
|
| 84 |
+
# Interactive mode
|
| 85 |
+
print("\n" + "="*80)
|
| 86 |
+
print("GENERATE NEW FLEETMIND API KEY")
|
| 87 |
+
print("="*80 + "\n")
|
| 88 |
+
|
| 89 |
+
email = input("Enter user email: ").strip()
|
| 90 |
+
if not email:
|
| 91 |
+
print("❌ Email is required")
|
| 92 |
+
sys.exit(1)
|
| 93 |
+
|
| 94 |
+
name = input("Enter user name (optional): ").strip() or None
|
| 95 |
+
else:
|
| 96 |
+
email = args.email
|
| 97 |
+
name = args.name
|
| 98 |
+
|
| 99 |
+
print(f"\nGenerating API key for {email}...")
|
| 100 |
+
result = generate_api_key(email, name)
|
| 101 |
+
|
| 102 |
+
if not result['success']:
|
| 103 |
+
print(f"\n❌ Error: {result['error']}\n")
|
| 104 |
+
sys.exit(1)
|
| 105 |
+
|
| 106 |
+
# Success! Display the API key
|
| 107 |
+
print("\n" + "="*80)
|
| 108 |
+
print("✅ API KEY GENERATED SUCCESSFULLY")
|
| 109 |
+
print("="*80 + "\n")
|
| 110 |
+
|
| 111 |
+
print(f"Email: {result['email']}")
|
| 112 |
+
print(f"Name: {result['name']}")
|
| 113 |
+
print(f"User ID: {result['user_id']}")
|
| 114 |
+
print(f"Created: {result['created_at']}")
|
| 115 |
+
print()
|
| 116 |
+
print("🔑 API KEY (copy this now - it won't be shown again!):")
|
| 117 |
+
print("-" * 80)
|
| 118 |
+
print(result['api_key'])
|
| 119 |
+
print("-" * 80)
|
| 120 |
+
print()
|
| 121 |
+
print("📋 Add this to your Claude Desktop config:")
|
| 122 |
+
print()
|
| 123 |
+
print(' {')
|
| 124 |
+
print(' "mcpServers": {')
|
| 125 |
+
print(' "fleetmind": {')
|
| 126 |
+
print(' "command": "npx",')
|
| 127 |
+
print(' "args": [')
|
| 128 |
+
print(' "mcp-remote",')
|
| 129 |
+
print(' "https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse"')
|
| 130 |
+
print(' ],')
|
| 131 |
+
print(' "env": {')
|
| 132 |
+
print(f' "FLEETMIND_API_KEY": "{result["api_key"]}"')
|
| 133 |
+
print(' }')
|
| 134 |
+
print(' }')
|
| 135 |
+
print(' }')
|
| 136 |
+
print(' }')
|
| 137 |
+
print()
|
| 138 |
+
print("⚠️ IMPORTANT: Save this API key securely. You won't be able to see it again!")
|
| 139 |
+
print()
|
| 140 |
+
|
| 141 |
+
if __name__ == "__main__":
|
| 142 |
+
main()
|
requirements.txt
CHANGED
|
@@ -17,7 +17,6 @@ psycopg2-binary>=2.9.9
|
|
| 17 |
# API Clients
|
| 18 |
googlemaps>=4.10.0
|
| 19 |
requests>=2.31.0
|
| 20 |
-
stytch>=8.0.0
|
| 21 |
|
| 22 |
# Utilities
|
| 23 |
python-dotenv>=1.0.0
|
|
|
|
| 17 |
# API Clients
|
| 18 |
googlemaps>=4.10.0
|
| 19 |
requests>=2.31.0
|
|
|
|
| 20 |
|
| 21 |
# Utilities
|
| 22 |
python-dotenv>=1.0.0
|
server.py
CHANGED
|
@@ -18,15 +18,13 @@ from datetime import datetime
|
|
| 18 |
sys.path.insert(0, str(Path(__file__).parent))
|
| 19 |
|
| 20 |
from fastmcp import FastMCP
|
| 21 |
-
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier, AccessToken
|
| 22 |
-
from pydantic import AnyHttpUrl
|
| 23 |
|
| 24 |
# Import existing services (unchanged)
|
| 25 |
from chat.geocoding import GeocodingService
|
| 26 |
from database.connection import execute_query, execute_write, test_connection
|
| 27 |
|
| 28 |
-
# Import
|
| 29 |
-
from database.user_context import
|
| 30 |
|
| 31 |
# Configure logging
|
| 32 |
logging.basicConfig(
|
|
@@ -39,65 +37,9 @@ logging.basicConfig(
|
|
| 39 |
)
|
| 40 |
logger = logging.getLogger(__name__)
|
| 41 |
|
| 42 |
-
# ============================================================================
|
| 43 |
-
# OAUTH AUTHENTICATION SETUP
|
| 44 |
-
# ============================================================================
|
| 45 |
-
|
| 46 |
-
class StytchTokenVerifier(TokenVerifier):
|
| 47 |
-
"""Token verifier for Stytch session tokens"""
|
| 48 |
-
|
| 49 |
-
def __init__(self):
|
| 50 |
-
super().__init__(
|
| 51 |
-
base_url=os.getenv('SERVER_URL', 'http://localhost:7860'),
|
| 52 |
-
required_scopes=None # Scope checking handled per-tool
|
| 53 |
-
)
|
| 54 |
-
|
| 55 |
-
async def verify_token(self, token: str) -> AccessToken | None:
|
| 56 |
-
"""Verify Stytch session token and return AccessToken"""
|
| 57 |
-
try:
|
| 58 |
-
# Use existing Stytch verification function
|
| 59 |
-
user_info = verify_token(token)
|
| 60 |
-
|
| 61 |
-
if not user_info:
|
| 62 |
-
logger.debug(f"Token verification failed")
|
| 63 |
-
return None
|
| 64 |
-
|
| 65 |
-
# Convert to FastMCP AccessToken format
|
| 66 |
-
access_token = AccessToken(
|
| 67 |
-
token=token,
|
| 68 |
-
client_id=user_info['user_id'],
|
| 69 |
-
scopes=user_info.get('scopes', []),
|
| 70 |
-
resource_owner=user_info.get('email'),
|
| 71 |
-
claims={
|
| 72 |
-
'user_id': user_info['user_id'],
|
| 73 |
-
'email': user_info['email'],
|
| 74 |
-
'name': user_info.get('name', 'Unknown User'),
|
| 75 |
-
}
|
| 76 |
-
)
|
| 77 |
-
|
| 78 |
-
logger.info(f"Token verified for user: {user_info['email']} (user_id: {user_info['user_id']})")
|
| 79 |
-
return access_token
|
| 80 |
-
|
| 81 |
-
except Exception as e:
|
| 82 |
-
logger.error(f"Token verification error: {e}")
|
| 83 |
-
return None
|
| 84 |
-
|
| 85 |
-
# Create OAuth authentication provider (but don't apply globally yet)
|
| 86 |
-
auth_provider = RemoteAuthProvider(
|
| 87 |
-
token_verifier=StytchTokenVerifier(),
|
| 88 |
-
authorization_servers=[AnyHttpUrl('https://test.stytch.com/v1/public')],
|
| 89 |
-
base_url=os.getenv('SERVER_URL', 'http://localhost:7860'),
|
| 90 |
-
resource_name="FleetMind Dispatch Coordinator"
|
| 91 |
-
)
|
| 92 |
-
|
| 93 |
-
logger.info("OAuth authentication provider configured with Stytch")
|
| 94 |
-
|
| 95 |
# ============================================================================
|
| 96 |
# MCP SERVER INITIALIZATION
|
| 97 |
# ============================================================================
|
| 98 |
-
|
| 99 |
-
# NOTE: Not using auth=auth_provider here because it would block SSE connections
|
| 100 |
-
# Instead, we manually verify tokens in each tool using get_authenticated_user()
|
| 101 |
mcp = FastMCP(
|
| 102 |
name="FleetMind Dispatch Coordinator",
|
| 103 |
version="1.0.0"
|
|
@@ -116,21 +58,36 @@ except Exception as e:
|
|
| 116 |
logger.error(f"Database: Connection failed - {e}")
|
| 117 |
|
| 118 |
# ============================================================================
|
| 119 |
-
# AUTHENTICATION
|
| 120 |
# ============================================================================
|
| 121 |
|
| 122 |
-
# Note: OAuth metadata endpoint will be added via app.py instead of here
|
| 123 |
-
# FastMCP doesn't expose direct app access for custom routes
|
| 124 |
-
|
| 125 |
def get_authenticated_user():
|
| 126 |
"""
|
| 127 |
-
Extract and verify user
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
Returns:
|
| 130 |
User info dict with user_id, email, scopes, name or None if not authenticated
|
| 131 |
"""
|
| 132 |
try:
|
| 133 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
if os.getenv("SKIP_AUTH") == "true":
|
| 135 |
logger.debug("SKIP_AUTH enabled - using development user")
|
| 136 |
return {
|
|
@@ -140,31 +97,10 @@ def get_authenticated_user():
|
|
| 140 |
'name': 'Development User'
|
| 141 |
}
|
| 142 |
|
| 143 |
-
#
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
access_token = get_access_token()
|
| 147 |
-
|
| 148 |
-
if not access_token:
|
| 149 |
-
logger.debug("No access token found in request context")
|
| 150 |
-
return None
|
| 151 |
-
|
| 152 |
-
# Token already verified by FastMCP auth middleware
|
| 153 |
-
# Extract user info from AccessToken claims
|
| 154 |
-
user_info = {
|
| 155 |
-
'user_id': access_token.claims.get('user_id') or access_token.client_id,
|
| 156 |
-
'email': access_token.resource_owner or access_token.claims.get('email', 'unknown'),
|
| 157 |
-
'scopes': access_token.scopes or [],
|
| 158 |
-
'name': access_token.claims.get('name', 'Unknown User')
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
logger.info(f"Authenticated user: {user_info['email']} (user_id: {user_info['user_id']})")
|
| 162 |
-
return user_info
|
| 163 |
-
|
| 164 |
-
except RuntimeError as e:
|
| 165 |
-
# No HTTP request context available (likely stdio mode or testing)
|
| 166 |
-
logger.debug(f"No request context available: {e}")
|
| 167 |
return None
|
|
|
|
| 168 |
except Exception as e:
|
| 169 |
logger.error(f"Authentication error: {e}")
|
| 170 |
return None
|
|
|
|
| 18 |
sys.path.insert(0, str(Path(__file__).parent))
|
| 19 |
|
| 20 |
from fastmcp import FastMCP
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# Import existing services (unchanged)
|
| 23 |
from chat.geocoding import GeocodingService
|
| 24 |
from database.connection import execute_query, execute_write, test_connection
|
| 25 |
|
| 26 |
+
# Import permission checking
|
| 27 |
+
from database.user_context import check_permission, get_required_scope
|
| 28 |
|
| 29 |
# Configure logging
|
| 30 |
logging.basicConfig(
|
|
|
|
| 37 |
)
|
| 38 |
logger = logging.getLogger(__name__)
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
# ============================================================================
|
| 41 |
# MCP SERVER INITIALIZATION
|
| 42 |
# ============================================================================
|
|
|
|
|
|
|
|
|
|
| 43 |
mcp = FastMCP(
|
| 44 |
name="FleetMind Dispatch Coordinator",
|
| 45 |
version="1.0.0"
|
|
|
|
| 58 |
logger.error(f"Database: Connection failed - {e}")
|
| 59 |
|
| 60 |
# ============================================================================
|
| 61 |
+
# AUTHENTICATION - API KEY SYSTEM
|
| 62 |
# ============================================================================
|
| 63 |
|
|
|
|
|
|
|
|
|
|
| 64 |
def get_authenticated_user():
|
| 65 |
"""
|
| 66 |
+
Extract and verify user via API Key authentication
|
| 67 |
+
|
| 68 |
+
Supports 2 authentication methods:
|
| 69 |
+
1. API Key (from FLEETMIND_API_KEY env var) - Production & user auth
|
| 70 |
+
2. Development Mode (SKIP_AUTH=true) - Local testing only
|
| 71 |
|
| 72 |
Returns:
|
| 73 |
User info dict with user_id, email, scopes, name or None if not authenticated
|
| 74 |
"""
|
| 75 |
try:
|
| 76 |
+
# METHOD 1: API Key Authentication
|
| 77 |
+
# Users set this in their Claude Desktop config:
|
| 78 |
+
# "env": {"FLEETMIND_API_KEY": "fm_xxxxx"}
|
| 79 |
+
api_key = os.getenv("FLEETMIND_API_KEY")
|
| 80 |
+
if api_key:
|
| 81 |
+
from database.api_keys import verify_api_key
|
| 82 |
+
user_info = verify_api_key(api_key)
|
| 83 |
+
if user_info:
|
| 84 |
+
logger.info(f"✅ API Key auth: {user_info['email']} (user_id: {user_info['user_id']})")
|
| 85 |
+
return user_info
|
| 86 |
+
else:
|
| 87 |
+
logger.warning(f"❌ Invalid API key provided")
|
| 88 |
+
return None
|
| 89 |
+
|
| 90 |
+
# METHOD 2: Development bypass mode (local testing only)
|
| 91 |
if os.getenv("SKIP_AUTH") == "true":
|
| 92 |
logger.debug("SKIP_AUTH enabled - using development user")
|
| 93 |
return {
|
|
|
|
| 97 |
'name': 'Development User'
|
| 98 |
}
|
| 99 |
|
| 100 |
+
# No authentication provided
|
| 101 |
+
logger.debug("No API key or SKIP_AUTH - authentication required")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
return None
|
| 103 |
+
|
| 104 |
except Exception as e:
|
| 105 |
logger.error(f"Authentication error: {e}")
|
| 106 |
return None
|