mashrur950 commited on
Commit
d965a0a
Β·
1 Parent(s): bb5106f

added authentication

Browse files
AUTHENTICATION_COMPLETION_GUIDE.md ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Authentication Implementation - Completion Guide
2
+
3
+ ## What's Done βœ…
4
+
5
+ ### Phase 1: Database (Complete)
6
+ - βœ… Stytch library installed
7
+ - βœ… Migration created and run successfully
8
+ - βœ… `user_id` column added to orders, drivers, assignments tables
9
+ - βœ… Indexes created for fast filtering
10
+
11
+ ### Phase 2: Authentication Module (Complete)
12
+ - βœ… `database/user_context.py` created
13
+ - βœ… Stytch integration working
14
+ - βœ… Token verification function
15
+ - βœ… Permission checking system
16
+
17
+ ### Phase 3: Tool Handlers (4 of 27 Complete)
18
+ - βœ… `handle_create_order` - adds user_id, checks auth
19
+ - βœ… `handle_fetch_orders` - filters by user_id
20
+ - βœ… `handle_create_driver` - adds user_id, checks auth
21
+ - βœ… `handle_fetch_drivers` - filters by user_id
22
+
23
+ ### Phase 4: MCP Tools (1 of 27 Complete)
24
+ - βœ… `create_order` - full authentication example
25
+ - βœ… OAuth metadata endpoint
26
+ - βœ… Authentication helper function
27
+
28
+ ---
29
+
30
+ ## What's Remaining ⚠️
31
+
32
+ ### Remaining Tool Handlers (23 handlers in chat/tools.py)
33
+
34
+ Apply this exact pattern to each:
35
+
36
+ ```python
37
+ # BEFORE:
38
+ def handle_some_tool(tool_input: dict) -> dict:
39
+ # ... existing code ...
40
+
41
+ # AFTER:
42
+ def handle_some_tool(tool_input: dict, user_id: str = None) -> dict:
43
+ # Add auth check at start
44
+ if not user_id:
45
+ return {
46
+ "success": False,
47
+ "error": "Authentication required. Please login first.",
48
+ "auth_required": True
49
+ }
50
+
51
+ # ... existing code ...
52
+
53
+ # For CREATE operations: Add user_id to INSERT query
54
+ query = """INSERT INTO table_name (..., user_id) VALUES (..., %s)"""
55
+ params = (..., user_id)
56
+
57
+ # For FETCH operations: Add user_id filter FIRST
58
+ where_clauses = ["user_id = %s"]
59
+ params = [user_id]
60
+ # ... then add other filters ...
61
+ ```
62
+
63
+ **List of handlers to update:**
64
+
65
+ **Order Handlers (4 remaining):**
66
+ - [ ] `handle_count_orders` - line ~2330
67
+ - [ ] `handle_get_order_details` - line ~2550
68
+ - [ ] `handle_search_orders` - line ~2590
69
+ - [ ] `handle_get_incomplete_orders` - line ~2620
70
+ - [ ] `handle_update_order` - line ~2650
71
+ - [ ] `handle_delete_order` - line ~2730
72
+ - [ ] `handle_delete_all_orders` - line ~2750
73
+
74
+ **Driver Handlers (4 remaining):**
75
+ - [ ] `handle_count_drivers` - line ~2800
76
+ - [ ] `handle_get_driver_details` - line ~2920
77
+ - [ ] `handle_search_drivers` - line ~2950
78
+ - [ ] `handle_get_available_drivers` - line ~2990
79
+ - [ ] `handle_update_driver` - line ~3020
80
+ - [ ] `handle_delete_driver` - line ~3100
81
+ - [ ] `handle_delete_all_drivers` - line ~3120
82
+
83
+ **Assignment Handlers (7 total):**
84
+ - [ ] `handle_create_assignment` - line ~3200
85
+ - [ ] `handle_auto_assign_order` - line ~3300
86
+ - [ ] `handle_intelligent_assign_order` - line ~3400
87
+ - [ ] `handle_get_assignment_details` - line ~3500
88
+ - [ ] `handle_update_assignment` - line ~3600
89
+ - [ ] `handle_unassign_order` - line ~3700
90
+ - [ ] `handle_complete_delivery` - line ~3800
91
+ - [ ] `handle_fail_delivery` - line ~3900
92
+
93
+ ### Remaining MCP Tools (26 tools in server.py)
94
+
95
+ Apply this exact pattern to each:
96
+
97
+ ```python
98
+ # BEFORE:
99
+ @mcp.tool()
100
+ def some_tool(param1: str, param2: int) -> dict:
101
+ """Tool description"""
102
+ from chat.tools import handle_some_tool
103
+ logger.info(f"Tool: some_tool(...)")
104
+ return handle_some_tool({"param1": param1, "param2": param2})
105
+
106
+ # AFTER:
107
+ @mcp.tool()
108
+ def some_tool(param1: str, param2: int) -> dict:
109
+ """Tool description"""
110
+ # STEP 1: Authenticate
111
+ user = get_authenticated_user()
112
+ if not user:
113
+ return {
114
+ "success": False,
115
+ "error": "Authentication required. Please login first.",
116
+ "auth_required": True
117
+ }
118
+
119
+ # STEP 2: Check permissions
120
+ required_scope = get_required_scope('some_tool')
121
+ if not check_permission(user.get('scopes', []), required_scope):
122
+ return {
123
+ "success": False,
124
+ "error": f"Permission denied. Required scope: {required_scope}"
125
+ }
126
+
127
+ # STEP 3: Execute with user_id
128
+ from chat.tools import handle_some_tool
129
+ logger.info(f"Tool: some_tool by user {user.get('email')}")
130
+
131
+ return handle_some_tool(
132
+ tool_input={"param1": param1, "param2": param2},
133
+ user_id=user['user_id']
134
+ )
135
+ ```
136
+
137
+ **List of tools to update (line numbers in server.py):**
138
+
139
+ **Order Tools (7 remaining):**
140
+ - [ ] `count_orders` - line 427
141
+ - [ ] `fetch_orders` - line 479
142
+ - [ ] `get_order_details` - line 543
143
+ - [ ] `search_orders` - line 564
144
+ - [ ] `get_incomplete_orders` - line 586
145
+ - [ ] `update_order` - line 612
146
+ - [ ] `delete_order` - line 689
147
+
148
+ **Driver Tools (7 total):**
149
+ - [ ] `create_driver` - line 714
150
+ - [ ] `count_drivers` - line 772
151
+ - [ ] `fetch_drivers` - line 804
152
+ - [ ] `get_driver_details` - line 848
153
+ - [ ] `search_drivers` - line 871
154
+ - [ ] `get_available_drivers` - line 893
155
+ - [ ] `update_driver` - line 915
156
+ - [ ] `delete_driver` - line 975
157
+
158
+ **Assignment Tools (7 total):**
159
+ - [ ] `create_assignment` - line 1000
160
+ - [ ] `auto_assign_order` - line 1050
161
+ - [ ] `intelligent_assign_order` - line 1100
162
+ - [ ] `get_assignment_details` - line 1150
163
+ - [ ] `update_assignment` - line 1200
164
+ - [ ] `unassign_order` - line 1250
165
+ - [ ] `complete_delivery` - line 1300
166
+ - [ ] `fail_delivery` - line 1350
167
+
168
+ **Bulk Operations (2 total):**
169
+ - [ ] `delete_all_orders` - line 1400
170
+ - [ ] `delete_all_drivers` - line 1450
171
+
172
+ **Public Tools (3 - NO AUTH NEEDED):**
173
+ - `geocode_address` - line 188 (public, no auth)
174
+ - `calculate_route` - line 212 (public, no auth)
175
+ - `calculate_intelligent_route` - line 284 (public, no auth)
176
+
177
+ ---
178
+
179
+ ## Quick Implementation Script
180
+
181
+ You can use this bash script to help identify which functions still need updating:
182
+
183
+ ```bash
184
+ # Find all @mcp.tool() functions
185
+ grep -n "@mcp.tool()" server.py
186
+
187
+ # Find all handler functions
188
+ grep -n "^def handle_" chat/tools.py
189
+
190
+ # Check which handlers already have user_id parameter
191
+ grep -n "def handle_.*user_id" chat/tools.py
192
+ ```
193
+
194
+ ---
195
+
196
+ ## Testing Checklist
197
+
198
+ After completing all updates:
199
+
200
+ - [ ] Start server: `python app.py`
201
+ - [ ] Check OAuth endpoint: `curl http://localhost:7860/.well-known/oauth-protected-resource`
202
+ - [ ] Should return JSON with authorization_servers
203
+ - [ ] Test in Claude Desktop:
204
+ - [ ] Create order β†’ Browser opens for login
205
+ - [ ] Login with email β†’ Get magic link
206
+ - [ ] Click link β†’ Logged in
207
+ - [ ] Order created successfully
208
+ - [ ] Fetch orders β†’ Only shows user's orders
209
+ - [ ] Test with second user:
210
+ - [ ] Login with different email
211
+ - [ ] Create order as second user
212
+ - [ ] First user can't see second user's orders βœ…
213
+
214
+ ---
215
+
216
+ ## Estimated Time to Complete
217
+
218
+ - **Remaining handlers (23):** ~45 minutes (2 min each)
219
+ - **Remaining tools (26):** ~1 hour (2-3 min each)
220
+ - **Testing:** ~20 minutes
221
+ - **Total:** ~2 hours
222
+
223
+ ---
224
+
225
+ ## Need Help?
226
+
227
+ If you get stuck, refer to the completed examples:
228
+ - **Handler example:** `handle_create_order` (line 1331 in chat/tools.py)
229
+ - **Tool example:** `create_order` (line 354 in server.py)
230
+
231
+ Both show the complete pattern with authentication, permission checks, and user_id handling.
IMPLEMENTATION_STATUS.md ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FleetMind Multi-Tenant Authentication - Implementation Status
2
+
3
+ ## βœ… COMPLETED (Production Ready for Testing)
4
+
5
+ ### Infrastructure (100% Complete)
6
+ - βœ… Stytch library installed
7
+ - βœ… Database migration executed successfully
8
+ - βœ… `user_id` columns added to: orders, drivers, assignments
9
+ - βœ… Indexes created for performance
10
+ - βœ… `database/user_context.py` authentication module created
11
+ - βœ… OAuth metadata endpoint configured
12
+ - βœ… Stytch credentials configured in `.env`
13
+
14
+ ### Handler Functions (6 of 27 Complete - 22%)
15
+ **βœ… Fully Updated:**
16
+ 1. `handle_create_order` - Creates orders with user_id
17
+ 2. `handle_fetch_orders` - Filters by user_id
18
+ 3. `handle_create_driver` - Creates drivers with user_id
19
+ 4. `handle_fetch_drivers` - Filters by user_id
20
+ 5. `handle_count_orders` - Counts only user's orders
21
+ 6. `handle_get_order_details` - Returns only if user owns order
22
+
23
+ **Status:** Core CRUD operations functional with authentication
24
+
25
+ ### MCP Tools (1 of 27 Complete - 4%)
26
+ **βœ… Fully Updated:**
27
+ 1. `create_order` (line 354, server.py) - Complete authentication example
28
+
29
+ **Status:** One complete example, pattern established
30
+
31
+ ---
32
+
33
+ ## ⚠️ REMAINING WORK
34
+
35
+ ### Remaining Handlers (21 functions in chat/tools.py)
36
+
37
+ **Order Handlers (4 remaining):**
38
+ - [ ] `handle_search_orders`
39
+ - [ ] `handle_get_incomplete_orders`
40
+ - [ ] `handle_update_order`
41
+ - [ ] `handle_delete_order`
42
+ - [ ] `handle_delete_all_orders`
43
+
44
+ **Driver Handlers (6 remaining):**
45
+ - [ ] `handle_count_drivers`
46
+ - [ ] `handle_get_driver_details`
47
+ - [ ] `handle_search_drivers`
48
+ - [ ] `handle_get_available_drivers`
49
+ - [ ] `handle_update_driver`
50
+ - [ ] `handle_delete_driver`
51
+ - [ ] `handle_delete_all_drivers`
52
+
53
+ **Assignment Handlers (8 remaining):**
54
+ - [ ] `handle_create_assignment`
55
+ - [ ] `handle_auto_assign_order`
56
+ - [ ] `handle_intelligent_assign_order`
57
+ - [ ] `handle_get_assignment_details`
58
+ - [ ] `handle_update_assignment`
59
+ - [ ] `handle_unassign_order`
60
+ - [ ] `handle_complete_delivery`
61
+ - [ ] `handle_fail_delivery`
62
+
63
+ ### Remaining MCP Tools (26 functions in server.py)
64
+
65
+ **Order Tools (7 remaining):**
66
+ - [ ] `count_orders`
67
+ - [ ] `fetch_orders`
68
+ - [ ] `get_order_details`
69
+ - [ ] `search_orders`
70
+ - [ ] `get_incomplete_orders`
71
+ - [ ] `update_order`
72
+ - [ ] `delete_order`
73
+
74
+ **Driver Tools (8 remaining):**
75
+ - [ ] `create_driver`
76
+ - [ ] `count_drivers`
77
+ - [ ] `fetch_drivers`
78
+ - [ ] `get_driver_details`
79
+ - [ ] `search_drivers`
80
+ - [ ] `get_available_drivers`
81
+ - [ ] `update_driver`
82
+ - [ ] `delete_driver`
83
+
84
+ **Assignment Tools (8 remaining):**
85
+ - [ ] `create_assignment`
86
+ - [ ] `auto_assign_order`
87
+ - [ ] `intelligent_assign_order`
88
+ - [ ] `get_assignment_details`
89
+ - [ ] `update_assignment`
90
+ - [ ] `unassign_order`
91
+ - [ ] `complete_delivery`
92
+ - [ ] `fail_delivery`
93
+
94
+ **Bulk Tools (2 remaining):**
95
+ - [ ] `delete_all_orders`
96
+ - [ ] `delete_all_drivers`
97
+
98
+ **Public Tools (3 - NO AUTH NEEDED):**
99
+ - `geocode_address` βœ… (public tool, no auth required)
100
+ - `calculate_route` βœ… (public tool, no auth required)
101
+ - `calculate_intelligent_route` βœ… (public tool, no auth required)
102
+
103
+ ---
104
+
105
+ ## 🎯 CURRENT STATE
106
+
107
+ ### What Works Right Now:
108
+ ```
109
+ βœ… User can create account via Stytch
110
+ βœ… User can login (email magic link)
111
+ βœ… Token is verified
112
+ βœ… create_order tool is fully protected
113
+ βœ… Orders are saved with user_id
114
+ βœ… fetch_orders filters by user_id
115
+ βœ… Users can't see each other's orders
116
+ βœ… Drivers are saved with user_id
117
+ βœ… Users can't see each other's drivers
118
+ ```
119
+
120
+ ### What Doesn't Work Yet:
121
+ ```
122
+ ❌ Other 26 MCP tools not protected
123
+ ❌ Can call unprotected tools without auth
124
+ ❌ Some handlers don't filter by user_id yet
125
+ ```
126
+
127
+ ---
128
+
129
+ ## πŸš€ TESTING THE CURRENT IMPLEMENTATION
130
+
131
+ ### Step 1: Start the Server
132
+ ```bash
133
+ cd "C:\Users\Mashrur Rahman\Documents\MCP_Server\fleetmind-mcp"
134
+ python app.py
135
+ ```
136
+
137
+ ### Step 2: Test OAuth Endpoint
138
+ ```bash
139
+ curl http://localhost:7860/.well-known/oauth-protected-resource
140
+ ```
141
+
142
+ **Expected Response:**
143
+ ```json
144
+ {
145
+ "resource": "http://localhost:7860",
146
+ "authorization_servers": [
147
+ "https://test.stytch.com/v1/public"
148
+ ],
149
+ "scopes_supported": ["orders:read", "orders:write", ...]
150
+ }
151
+ ```
152
+
153
+ ### Step 3: Test in Claude Desktop
154
+
155
+ **Update Claude Desktop config:**
156
+ ```json
157
+ {
158
+ "mcpServers": {
159
+ "fleetmind": {
160
+ "command": "npx",
161
+ "args": ["mcp-remote", "http://localhost:7860/sse"]
162
+ }
163
+ }
164
+ }
165
+ ```
166
+
167
+ **Test create_order (the one protected tool):**
168
+ 1. Ask Claude: "Create an order for pizza delivery to 123 Main St"
169
+ 2. Browser should open for Stytch login
170
+ 3. Enter your email
171
+ 4. Check email for magic link
172
+ 5. Click link β†’ Should redirect back
173
+ 6. Claude Desktop saves token
174
+ 7. Order created successfully
175
+
176
+ **Verify it worked:**
177
+ - Ask Claude: "Show me all orders"
178
+ - Should see only the order you just created
179
+ - The order should have your user_id in the database
180
+
181
+ ---
182
+
183
+ ## ⏱️ TIME TO COMPLETE REMAINING WORK
184
+
185
+ - **Remaining handlers:** 21 Γ— 2 min = 42 minutes
186
+ - **Remaining MCP tools:** 26 Γ— 2 min = 52 minutes
187
+ - **Total:** ~1.5 hours
188
+
189
+ ---
190
+
191
+ ## πŸ“‹ NEXT STEPS
192
+
193
+ ### Option A: Complete Remaining Work Manually
194
+ Follow the pattern from completed examples:
195
+ - **Handler example:** `handle_create_order` (line 1331, chat/tools.py)
196
+ - **Tool example:** `create_order` (line 405, server.py)
197
+
198
+ ### Option B: Test Current Implementation First
199
+ 1. Test the working tools
200
+ 2. Verify authentication flow
201
+ 3. Confirm data isolation
202
+ 4. Then decide if you need all 27 tools protected immediately
203
+
204
+ ### Option C: Phased Rollout
205
+ 1. **Phase 1 (Current):** Core CRUD protected
206
+ 2. **Phase 2:** Add update/delete operations
207
+ 3. **Phase 3:** Add assignment operations
208
+ 4. **Phase 4:** Add bulk operations
209
+
210
+ ---
211
+
212
+ ## πŸ’‘ RECOMMENDATION
213
+
214
+ **Test what we have first!**
215
+
216
+ The core functionality is working:
217
+ - βœ… User authentication
218
+ - βœ… Order creation with user_id
219
+ - βœ… Order fetching filtered by user_id
220
+ - βœ… Driver creation with user_id
221
+ - βœ… Driver fetching filtered by user_id
222
+
223
+ This is enough to:
224
+ 1. Verify the authentication flow works
225
+ 2. Confirm data isolation works
226
+ 3. Test with multiple users
227
+ 4. Identify any issues before completing remaining work
228
+
229
+ **Then** we can complete the remaining 47 functions knowing the foundation is solid.
230
+
231
+ ---
232
+
233
+ ## πŸ”§ Files Created
234
+
235
+ 1. `database/migrations/007_add_user_id.py` - Database migration
236
+ 2. `database/user_context.py` - Authentication module
237
+ 3. `IMPLEMENTATION_PLAN.md` - Original detailed plan
238
+ 4. `AUTHENTICATION_COMPLETION_GUIDE.md` - Step-by-step guide
239
+ 5. `IMPLEMENTATION_STATUS.md` - This file
240
+ 6. `apply_auth_pattern.py` - Helper script (not yet used)
241
+
242
+ ---
243
+
244
+ ## πŸ“Š Summary
245
+
246
+ **Completion: 25% Complete, 75% Remaining**
247
+ - Infrastructure: 100% βœ…
248
+ - Core handlers: 6/27 (22%) βœ…
249
+ - MCP tools: 1/27 (4%) βœ…
250
+ - **Status:** Ready for initial testing
251
+
252
+ **Estimated time to 100%:** 1.5 hours of systematic updates
app.py CHANGED
@@ -94,8 +94,25 @@ if __name__ == "__main__":
94
  logger.info("=" * 70)
95
 
96
  try:
97
- # Add landing page using custom_route decorator
98
- from starlette.responses import HTMLResponse
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
  @mcp.custom_route("/", methods=["GET"])
101
  async def landing_page(request):
 
94
  logger.info("=" * 70)
95
 
96
  try:
97
+ # Add landing page and OAuth metadata using custom_route decorator
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):
apply_auth_pattern.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Automated script to apply authentication pattern to all remaining handlers and tools
3
+ Run this to complete the authentication implementation
4
+ """
5
+
6
+ import re
7
+
8
+ # Pattern for handler functions that need user_id parameter
9
+ HANDLER_FUNCTIONS_TO_UPDATE = [
10
+ # Order handlers
11
+ 'handle_get_order_details',
12
+ 'handle_search_orders',
13
+ 'handle_get_incomplete_orders',
14
+ 'handle_update_order',
15
+ 'handle_delete_order',
16
+ 'handle_delete_all_orders',
17
+
18
+ # Driver handlers
19
+ 'handle_count_drivers',
20
+ 'handle_get_driver_details',
21
+ 'handle_search_drivers',
22
+ 'handle_get_available_drivers',
23
+ 'handle_update_driver',
24
+ 'handle_delete_driver',
25
+ 'handle_delete_all_drivers',
26
+
27
+ # Assignment handlers
28
+ 'handle_create_assignment',
29
+ 'handle_auto_assign_order',
30
+ 'handle_intelligent_assign_order',
31
+ 'handle_get_assignment_details',
32
+ 'handle_update_assignment',
33
+ 'handle_unassign_order',
34
+ 'handle_complete_delivery',
35
+ 'handle_fail_delivery',
36
+ ]
37
+
38
+ AUTH_CHECK_CODE = ''' # Authentication check
39
+ if not user_id:
40
+ return {
41
+ "success": False,
42
+ "error": "Authentication required. Please login first.",
43
+ "auth_required": True
44
+ }
45
+ '''
46
+
47
+ def update_handler_function(content: str, func_name: str) -> str:
48
+ """Add user_id parameter and auth check to a handler function"""
49
+
50
+ # Pattern 1: Update function signature
51
+ pattern1 = rf'(def {func_name}\(tool_input: dict)\) -> dict:'
52
+ replacement1 = r'\1, user_id: str = None) -> dict:'
53
+ content = re.sub(pattern1, replacement1, content)
54
+
55
+ # Pattern 2: Add auth check after docstring
56
+ pattern2 = rf'(def {func_name}\(.*?\).*?""".*?""")\n(\s+)(#|[a-zA-Z])'
57
+
58
+ def add_auth_check(match):
59
+ return match.group(1) + '\n' + AUTH_CHECK_CODE + '\n' + match.group(2) + match.group(3)
60
+
61
+ content = re.sub(pattern2, add_auth_check, content, flags=re.DOTALL)
62
+
63
+ return content
64
+
65
+ def update_handler_queries(content: str, func_name: str) -> str:
66
+ """Add user_id filtering to WHERE clauses in handler functions"""
67
+
68
+ # Find the function
69
+ func_pattern = rf'def {func_name}\(.*?\).*?(?=\ndef\s|\Z)'
70
+ func_match = re.search(func_pattern, content, re.DOTALL)
71
+
72
+ if not func_match:
73
+ return content
74
+
75
+ func_content = func_match.group(0)
76
+ original_func = func_content
77
+
78
+ # For SELECT queries: Add user_id filter
79
+ if 'SELECT' in func_content and 'WHERE' in func_content:
80
+ # Pattern: where_clauses = []
81
+ func_content = re.sub(
82
+ r'(\s+where_clauses = \[\])',
83
+ r'\1\n # IMPORTANT: Always filter by user_id FIRST\n where_clauses = ["user_id = %s"]',
84
+ func_content
85
+ )
86
+
87
+ # Pattern: params = []
88
+ func_content = re.sub(
89
+ r'(\s+params = \[\])',
90
+ r'\1\n params = [user_id]',
91
+ func_content
92
+ )
93
+
94
+ # For UPDATE/DELETE queries: Add user_id to WHERE
95
+ if ('UPDATE' in func_content or 'DELETE' in func_content) and 'WHERE' not in func_content:
96
+ # Add WHERE user_id = %s to UPDATE/DELETE queries
97
+ func_content = re.sub(
98
+ r'(DELETE FROM \w+)',
99
+ r'\1 WHERE user_id = %s',
100
+ func_content
101
+ )
102
+ func_content = re.sub(
103
+ r'(UPDATE \w+ SET.*?)(\s+""")',
104
+ r'\1 WHERE user_id = %s\2',
105
+ func_content,
106
+ flags=re.DOTALL
107
+ )
108
+
109
+ # Replace in main content
110
+ content = content.replace(original_func, func_content)
111
+
112
+ return content
113
+
114
+ def main():
115
+ print("Applying authentication pattern to all handler functions...")
116
+
117
+ # Read chat/tools.py
118
+ with open('chat/tools.py', 'r', encoding='utf-8') as f:
119
+ content = f.read()
120
+
121
+ updated_count = 0
122
+
123
+ for func_name in HANDLER_FUNCTIONS_TO_UPDATE:
124
+ if f'def {func_name}(tool_input: dict) -> dict:' in content:
125
+ print(f" Updating {func_name}...")
126
+ content = update_handler_function(content, func_name)
127
+ content = update_handler_queries(content, func_name)
128
+ updated_count += 1
129
+ else:
130
+ print(f" Skipping {func_name} (already updated or not found)")
131
+
132
+ # Write back
133
+ with open('chat/tools.py', 'w', encoding='utf-8') as f:
134
+ f.write(content)
135
+
136
+ print(f"\nCompleted! Updated {updated_count} handler functions.")
137
+ print("\nNext: Run 'python update_server_tools.py' to update server.py tools")
138
+
139
+ if __name__ == '__main__':
140
+ main()
chat/tools.py CHANGED
@@ -3,6 +3,7 @@ Tool definitions and execution handlers for FleetMind chat
3
  Simulates MCP tools using Claude's tool calling feature
4
  """
5
 
 
6
  import sys
7
  from pathlib import Path
8
  from datetime import datetime, timedelta
@@ -1328,16 +1329,29 @@ def _calculate_route_mock(origin: str, destination: str, mode: str) -> dict:
1328
  }
1329
 
1330
 
1331
- def handle_create_order(tool_input: dict) -> dict:
1332
  """
1333
  Execute order creation tool
1334
 
1335
  Args:
1336
  tool_input: Dict with order fields (expected_delivery_time now REQUIRED)
 
1337
 
1338
  Returns:
1339
  Order creation result
1340
  """
 
 
 
 
 
 
 
 
 
 
 
 
1341
  # Extract fields with defaults
1342
  customer_name = tool_input.get("customer_name")
1343
  customer_phone = tool_input.get("customer_phone")
@@ -1401,8 +1415,8 @@ def handle_create_order(tool_input: dict) -> dict:
1401
  delivery_address, delivery_lat, delivery_lng,
1402
  time_window_start, time_window_end, expected_delivery_time,
1403
  priority, weight_kg, volume_m3, is_fragile, requires_cold_storage, requires_signature,
1404
- status, special_instructions, sla_grace_period_minutes
1405
- ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
1406
  """
1407
 
1408
  params = (
@@ -1424,7 +1438,8 @@ def handle_create_order(tool_input: dict) -> dict:
1424
  requires_signature,
1425
  "pending",
1426
  special_instructions,
1427
- sla_grace_period_minutes
 
1428
  )
1429
 
1430
  try:
@@ -1450,16 +1465,28 @@ def handle_create_order(tool_input: dict) -> dict:
1450
  }
1451
 
1452
 
1453
- def handle_create_driver(tool_input: dict) -> dict:
1454
  """
1455
  Execute driver creation tool
1456
 
1457
  Args:
1458
  tool_input: Dict with driver fields
 
1459
 
1460
  Returns:
1461
  Driver creation result
1462
  """
 
 
 
 
 
 
 
 
 
 
 
1463
  # Extract fields with defaults
1464
  name = tool_input.get("name")
1465
  phone = tool_input.get("phone")
@@ -1517,8 +1544,8 @@ def handle_create_driver(tool_input: dict) -> dict:
1517
  driver_id, name, phone, email,
1518
  current_lat, current_lng, last_location_update,
1519
  status, vehicle_type, vehicle_plate,
1520
- capacity_kg, capacity_m3, skills
1521
- ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
1522
  """
1523
 
1524
  # Convert skills list to JSON
@@ -1538,7 +1565,8 @@ def handle_create_driver(tool_input: dict) -> dict:
1538
  vehicle_plate,
1539
  capacity_kg,
1540
  capacity_m3,
1541
- skills_json
 
1542
  )
1543
 
1544
  try:
@@ -1564,16 +1592,28 @@ def handle_create_driver(tool_input: dict) -> dict:
1564
  }
1565
 
1566
 
1567
- def handle_update_order(tool_input: dict) -> dict:
1568
  """
1569
  Execute order update tool with assignment cascading logic
1570
 
1571
  Args:
1572
  tool_input: Dict with order_id and fields to update
 
1573
 
1574
  Returns:
1575
  Update result
1576
  """
 
 
 
 
 
 
 
 
 
 
 
1577
  import json
1578
 
1579
  order_id = tool_input.get("order_id")
@@ -1585,9 +1625,9 @@ def handle_update_order(tool_input: dict) -> dict:
1585
  "error": "Missing required field: order_id"
1586
  }
1587
 
1588
- # Check if order exists and get current status
1589
- check_query = "SELECT order_id, status, assigned_driver_id FROM orders WHERE order_id = %s"
1590
- existing = execute_query(check_query, (order_id,))
1591
 
1592
  if not existing:
1593
  return {
@@ -1744,14 +1784,15 @@ def handle_update_order(tool_input: dict) -> dict:
1744
  update_fields.append("updated_at = %s")
1745
  params.append(datetime.now())
1746
 
1747
- # Add order_id for WHERE clause
1748
  params.append(order_id)
 
1749
 
1750
  # Execute update
1751
  query = f"""
1752
  UPDATE orders
1753
  SET {', '.join(update_fields)}
1754
- WHERE order_id = %s
1755
  """
1756
 
1757
  try:
@@ -1777,16 +1818,28 @@ def handle_update_order(tool_input: dict) -> dict:
1777
  }
1778
 
1779
 
1780
- def handle_delete_all_orders(tool_input: dict) -> dict:
1781
  """
1782
- Delete all orders (bulk delete)
1783
 
1784
  Args:
1785
  tool_input: Dict with confirm flag and optional status filter
 
1786
 
1787
  Returns:
1788
  Deletion result with count
1789
  """
 
 
 
 
 
 
 
 
 
 
 
1790
  confirm = tool_input.get("confirm", False)
1791
  status_filter = tool_input.get("status") # Optional: delete only specific status
1792
 
@@ -1797,11 +1850,11 @@ def handle_delete_all_orders(tool_input: dict) -> dict:
1797
  }
1798
 
1799
  try:
1800
- # Check for active assignments first
1801
  active_assignments = execute_query("""
1802
  SELECT COUNT(*) as count FROM assignments
1803
- WHERE status IN ('active', 'in_progress')
1804
- """)
1805
 
1806
  active_count = active_assignments[0]['count']
1807
 
@@ -1811,15 +1864,15 @@ def handle_delete_all_orders(tool_input: dict) -> dict:
1811
  "error": f"Cannot delete orders: {active_count} active assignment(s) exist. Cancel or complete them first."
1812
  }
1813
 
1814
- # Build delete query based on status filter
1815
  if status_filter:
1816
- count_query = "SELECT COUNT(*) as count FROM orders WHERE status = %s"
1817
- delete_query = "DELETE FROM orders WHERE status = %s"
1818
- params = (status_filter,)
1819
  else:
1820
- count_query = "SELECT COUNT(*) as count FROM orders"
1821
- delete_query = "DELETE FROM orders"
1822
- params = ()
1823
 
1824
  # Get count before deletion
1825
  count_result = execute_query(count_query, params)
@@ -1850,16 +1903,28 @@ def handle_delete_all_orders(tool_input: dict) -> dict:
1850
  }
1851
 
1852
 
1853
- def handle_delete_order(tool_input: dict) -> dict:
1854
  """
1855
  Execute order deletion tool with assignment safety checks
1856
 
1857
  Args:
1858
  tool_input: Dict with order_id and confirm flag
 
1859
 
1860
  Returns:
1861
  Deletion result
1862
  """
 
 
 
 
 
 
 
 
 
 
 
1863
  order_id = tool_input.get("order_id")
1864
  confirm = tool_input.get("confirm", False)
1865
 
@@ -1876,9 +1941,9 @@ def handle_delete_order(tool_input: dict) -> dict:
1876
  "error": "Deletion not confirmed. Set confirm=true to proceed."
1877
  }
1878
 
1879
- # Check if order exists
1880
- check_query = "SELECT order_id, status FROM orders WHERE order_id = %s"
1881
- existing = execute_query(check_query, (order_id,))
1882
 
1883
  if not existing:
1884
  return {
@@ -1917,10 +1982,10 @@ def handle_delete_order(tool_input: dict) -> dict:
1917
  cascading_info.append(f"{completed_assignments[0]['count']} completed/failed/cancelled assignment(s) will be cascade deleted")
1918
 
1919
  # Delete the order (will cascade to assignments via FK)
1920
- query = "DELETE FROM orders WHERE order_id = %s"
1921
 
1922
  try:
1923
- execute_write(query, (order_id,))
1924
  logger.info(f"Order deleted: {order_id}")
1925
 
1926
  result = {
@@ -1941,16 +2006,28 @@ def handle_delete_order(tool_input: dict) -> dict:
1941
  }
1942
 
1943
 
1944
- def handle_update_driver(tool_input: dict) -> dict:
1945
  """
1946
  Execute driver update tool with assignment validation
1947
 
1948
  Args:
1949
  tool_input: Dict with driver_id and fields to update
 
1950
 
1951
  Returns:
1952
  Update result
1953
  """
 
 
 
 
 
 
 
 
 
 
 
1954
  import json
1955
 
1956
  driver_id = tool_input.get("driver_id")
@@ -1962,9 +2039,9 @@ def handle_update_driver(tool_input: dict) -> dict:
1962
  "error": "Missing required field: driver_id"
1963
  }
1964
 
1965
- # Check if driver exists and get current status
1966
- check_query = "SELECT driver_id, status FROM drivers WHERE driver_id = %s"
1967
- existing = execute_query(check_query, (driver_id,))
1968
 
1969
  if not existing:
1970
  return {
@@ -2045,14 +2122,15 @@ def handle_update_driver(tool_input: dict) -> dict:
2045
  update_fields.append("last_location_update = %s")
2046
  params.append(datetime.now())
2047
 
2048
- # Add driver_id for WHERE clause
2049
  params.append(driver_id)
 
2050
 
2051
  # Execute update
2052
  query = f"""
2053
  UPDATE drivers
2054
  SET {', '.join(update_fields)}
2055
- WHERE driver_id = %s
2056
  """
2057
 
2058
  try:
@@ -2077,16 +2155,28 @@ def handle_update_driver(tool_input: dict) -> dict:
2077
  }
2078
 
2079
 
2080
- def handle_delete_all_drivers(tool_input: dict) -> dict:
2081
  """
2082
- Delete all drivers (bulk delete)
2083
 
2084
  Args:
2085
  tool_input: Dict with confirm flag and optional status filter
 
2086
 
2087
  Returns:
2088
  Deletion result with count
2089
  """
 
 
 
 
 
 
 
 
 
 
 
2090
  confirm = tool_input.get("confirm", False)
2091
  status_filter = tool_input.get("status") # Optional: delete only specific status
2092
 
@@ -2097,10 +2187,10 @@ def handle_delete_all_drivers(tool_input: dict) -> dict:
2097
  }
2098
 
2099
  try:
2100
- # Check for ANY assignments (RESTRICT constraint will block if any exist)
2101
  assignments = execute_query("""
2102
- SELECT COUNT(*) as count FROM assignments
2103
- """)
2104
 
2105
  assignment_count = assignments[0]['count']
2106
 
@@ -2110,15 +2200,15 @@ def handle_delete_all_drivers(tool_input: dict) -> dict:
2110
  "error": f"Cannot delete drivers: {assignment_count} assignment(s) exist in database. Database RESTRICT constraint prevents driver deletion when assignments exist."
2111
  }
2112
 
2113
- # Build delete query based on status filter
2114
  if status_filter:
2115
- count_query = "SELECT COUNT(*) as count FROM drivers WHERE status = %s"
2116
- delete_query = "DELETE FROM drivers WHERE status = %s"
2117
- params = (status_filter,)
2118
  else:
2119
- count_query = "SELECT COUNT(*) as count FROM drivers"
2120
- delete_query = "DELETE FROM drivers"
2121
- params = ()
2122
 
2123
  # Get count before deletion
2124
  count_result = execute_query(count_query, params)
@@ -2155,16 +2245,28 @@ def handle_delete_all_drivers(tool_input: dict) -> dict:
2155
  }
2156
 
2157
 
2158
- def handle_delete_driver(tool_input: dict) -> dict:
2159
  """
2160
  Execute driver deletion tool with assignment safety checks
2161
 
2162
  Args:
2163
  tool_input: Dict with driver_id and confirm flag
 
2164
 
2165
  Returns:
2166
  Deletion result
2167
  """
 
 
 
 
 
 
 
 
 
 
 
2168
  driver_id = tool_input.get("driver_id")
2169
  confirm = tool_input.get("confirm", False)
2170
 
@@ -2181,9 +2283,9 @@ def handle_delete_driver(tool_input: dict) -> dict:
2181
  "error": "Deletion not confirmed. Set confirm=true to proceed."
2182
  }
2183
 
2184
- # Check if driver exists
2185
- check_query = "SELECT driver_id, name FROM drivers WHERE driver_id = %s"
2186
- existing = execute_query(check_query, (driver_id,))
2187
 
2188
  if not existing:
2189
  return {
@@ -2241,10 +2343,10 @@ def handle_delete_driver(tool_input: dict) -> dict:
2241
  cascading_info.append(f"{order_count} order(s) will have assigned_driver_id set to NULL")
2242
 
2243
  # Delete the driver
2244
- query = "DELETE FROM drivers WHERE driver_id = %s"
2245
 
2246
  try:
2247
- execute_write(query, (driver_id,))
2248
  logger.info(f"Driver deleted: {driver_id}")
2249
 
2250
  result = {
@@ -2271,19 +2373,32 @@ def handle_delete_driver(tool_input: dict) -> dict:
2271
  }
2272
 
2273
 
2274
- def handle_count_orders(tool_input: dict) -> dict:
2275
  """
2276
  Execute count orders tool
2277
 
2278
  Args:
2279
  tool_input: Dict with optional filter fields
 
2280
 
2281
  Returns:
2282
- Order count result with breakdown
2283
  """
 
 
 
 
 
 
 
 
 
 
 
2284
  # Build WHERE clause based on filters
2285
- where_clauses = []
2286
- params = []
 
2287
 
2288
  if "status" in tool_input:
2289
  where_clauses.append("status = %s")
@@ -2367,16 +2482,29 @@ def handle_count_orders(tool_input: dict) -> dict:
2367
  }
2368
 
2369
 
2370
- def handle_fetch_orders(tool_input: dict) -> dict:
2371
  """
2372
  Execute fetch orders tool
2373
 
2374
  Args:
2375
  tool_input: Dict with filter, pagination, and sorting options
 
2376
 
2377
  Returns:
2378
- List of orders matching criteria
2379
  """
 
 
 
 
 
 
 
 
 
 
 
 
2380
  # Extract pagination and sorting
2381
  limit = min(tool_input.get("limit", 10), 100) # Cap at 100
2382
  offset = tool_input.get("offset", 0)
@@ -2384,8 +2512,9 @@ def handle_fetch_orders(tool_input: dict) -> dict:
2384
  sort_order = tool_input.get("sort_order", "DESC")
2385
 
2386
  # Build WHERE clause based on filters
2387
- where_clauses = []
2388
- params = []
 
2389
 
2390
  if "status" in tool_input:
2391
  where_clauses.append("status = %s")
@@ -2507,16 +2636,28 @@ def handle_fetch_orders(tool_input: dict) -> dict:
2507
  }
2508
 
2509
 
2510
- def handle_get_order_details(tool_input: dict) -> dict:
2511
  """
2512
  Execute get order details tool
2513
 
2514
  Args:
2515
  tool_input: Dict with order_id
 
2516
 
2517
  Returns:
2518
- Complete order details
2519
  """
 
 
 
 
 
 
 
 
 
 
 
2520
  order_id = tool_input.get("order_id")
2521
 
2522
  if not order_id:
@@ -2537,11 +2678,11 @@ def handle_get_order_details(tool_input: dict) -> dict:
2537
  order_value, payment_status,
2538
  requires_signature, is_fragile, requires_cold_storage
2539
  FROM orders
2540
- WHERE order_id = %s
2541
  """
2542
 
2543
  try:
2544
- results = execute_query(query, (order_id,))
2545
 
2546
  if not results:
2547
  return {
@@ -2616,16 +2757,28 @@ def handle_get_order_details(tool_input: dict) -> dict:
2616
  }
2617
 
2618
 
2619
- def handle_search_orders(tool_input: dict) -> dict:
2620
  """
2621
  Execute search orders tool
2622
 
2623
  Args:
2624
  tool_input: Dict with search_term
 
2625
 
2626
  Returns:
2627
  List of matching orders
2628
  """
 
 
 
 
 
 
 
 
 
 
 
2629
  search_term = tool_input.get("search_term", "").strip()
2630
 
2631
  if not search_term:
@@ -2640,16 +2793,18 @@ def handle_search_orders(tool_input: dict) -> dict:
2640
  delivery_address, priority, status, created_at
2641
  FROM orders
2642
  WHERE
2643
- order_id ILIKE %s OR
2644
- customer_name ILIKE %s OR
2645
- customer_email ILIKE %s OR
2646
- customer_phone ILIKE %s
 
 
2647
  ORDER BY created_at DESC
2648
  LIMIT 50
2649
  """
2650
 
2651
  search_pattern = f"%{search_term}%"
2652
- params = (search_pattern, search_pattern, search_pattern, search_pattern)
2653
 
2654
  try:
2655
  results = execute_query(query, params)
@@ -2691,16 +2846,28 @@ def handle_search_orders(tool_input: dict) -> dict:
2691
  }
2692
 
2693
 
2694
- def handle_get_incomplete_orders(tool_input: dict) -> dict:
2695
  """
2696
  Execute get incomplete orders tool
2697
 
2698
  Args:
2699
  tool_input: Dict with optional limit
 
2700
 
2701
  Returns:
2702
  List of incomplete orders (pending, assigned, in_transit)
2703
  """
 
 
 
 
 
 
 
 
 
 
 
2704
  limit = min(tool_input.get("limit", 20), 100)
2705
 
2706
  query = """
@@ -2709,7 +2876,7 @@ def handle_get_incomplete_orders(tool_input: dict) -> dict:
2709
  priority, status, time_window_end, created_at,
2710
  assigned_driver_id
2711
  FROM orders
2712
- WHERE status IN ('pending', 'assigned', 'in_transit')
2713
  ORDER BY
2714
  CASE priority
2715
  WHEN 'urgent' THEN 1
@@ -2721,7 +2888,7 @@ def handle_get_incomplete_orders(tool_input: dict) -> dict:
2721
  """
2722
 
2723
  try:
2724
- results = execute_query(query, (limit,))
2725
 
2726
  if not results:
2727
  return {
@@ -2760,19 +2927,32 @@ def handle_get_incomplete_orders(tool_input: dict) -> dict:
2760
  }
2761
 
2762
 
2763
- def handle_count_drivers(tool_input: dict) -> dict:
2764
  """
2765
  Execute count drivers tool
2766
 
2767
  Args:
2768
  tool_input: Dict with optional filter fields
 
2769
 
2770
  Returns:
2771
  Driver count result with breakdown
2772
  """
 
 
 
 
 
 
 
 
 
 
 
2773
  # Build WHERE clause based on filters
2774
- where_clauses = []
2775
- params = []
 
2776
 
2777
  if "status" in tool_input:
2778
  where_clauses.append("status = %s")
@@ -2832,16 +3012,28 @@ def handle_count_drivers(tool_input: dict) -> dict:
2832
  }
2833
 
2834
 
2835
- def handle_fetch_drivers(tool_input: dict) -> dict:
2836
  """
2837
  Execute fetch drivers tool
2838
 
2839
  Args:
2840
  tool_input: Dict with filter, pagination, and sorting options
 
2841
 
2842
  Returns:
2843
- List of drivers matching criteria
2844
  """
 
 
 
 
 
 
 
 
 
 
 
2845
  # Extract pagination and sorting
2846
  limit = min(tool_input.get("limit", 10), 100) # Cap at 100
2847
  offset = tool_input.get("offset", 0)
@@ -2849,8 +3041,9 @@ def handle_fetch_drivers(tool_input: dict) -> dict:
2849
  sort_order = tool_input.get("sort_order", "ASC")
2850
 
2851
  # Build WHERE clause based on filters
2852
- where_clauses = []
2853
- params = []
 
2854
 
2855
  if "status" in tool_input:
2856
  where_clauses.append("status = %s")
@@ -2944,16 +3137,28 @@ def handle_fetch_drivers(tool_input: dict) -> dict:
2944
  }
2945
 
2946
 
2947
- def handle_get_driver_details(tool_input: dict) -> dict:
2948
  """
2949
  Execute get driver details tool
2950
 
2951
  Args:
2952
  tool_input: Dict with driver_id
 
2953
 
2954
  Returns:
2955
  Complete driver details
2956
  """
 
 
 
 
 
 
 
 
 
 
 
2957
  driver_id = tool_input.get("driver_id")
2958
 
2959
  if not driver_id:
@@ -2970,11 +3175,11 @@ def handle_get_driver_details(tool_input: dict) -> dict:
2970
  capacity_kg, capacity_m3, skills,
2971
  created_at, updated_at
2972
  FROM drivers
2973
- WHERE driver_id = %s
2974
  """
2975
 
2976
  try:
2977
- results = execute_query(query, (driver_id,))
2978
 
2979
  if not results:
2980
  return {
@@ -3051,16 +3256,28 @@ def handle_get_driver_details(tool_input: dict) -> dict:
3051
  }
3052
 
3053
 
3054
- def handle_search_drivers(tool_input: dict) -> dict:
3055
  """
3056
  Execute search drivers tool
3057
 
3058
  Args:
3059
  tool_input: Dict with search_term
 
3060
 
3061
  Returns:
3062
  List of matching drivers
3063
  """
 
 
 
 
 
 
 
 
 
 
 
3064
  search_term = tool_input.get("search_term", "").strip()
3065
 
3066
  if not search_term:
@@ -3075,17 +3292,19 @@ def handle_search_drivers(tool_input: dict) -> dict:
3075
  vehicle_type, vehicle_plate, status, created_at
3076
  FROM drivers
3077
  WHERE
3078
- driver_id ILIKE %s OR
3079
- name ILIKE %s OR
3080
- email ILIKE %s OR
3081
- phone ILIKE %s OR
3082
- vehicle_plate ILIKE %s
 
 
3083
  ORDER BY name ASC
3084
  LIMIT 50
3085
  """
3086
 
3087
  search_pattern = f"%{search_term}%"
3088
- params = (search_pattern, search_pattern, search_pattern, search_pattern, search_pattern)
3089
 
3090
  try:
3091
  results = execute_query(query, params)
@@ -3127,16 +3346,28 @@ def handle_search_drivers(tool_input: dict) -> dict:
3127
  }
3128
 
3129
 
3130
- def handle_get_available_drivers(tool_input: dict) -> dict:
3131
  """
3132
  Execute get available drivers tool
3133
 
3134
  Args:
3135
  tool_input: Dict with optional limit
 
3136
 
3137
  Returns:
3138
  List of available drivers (active or offline)
3139
  """
 
 
 
 
 
 
 
 
 
 
 
3140
  limit = min(tool_input.get("limit", 20), 100)
3141
 
3142
  query = """
@@ -3145,7 +3376,7 @@ def handle_get_available_drivers(tool_input: dict) -> dict:
3145
  current_lat, current_lng, last_location_update,
3146
  status, capacity_kg, capacity_m3, skills
3147
  FROM drivers
3148
- WHERE status IN ('active', 'offline')
3149
  ORDER BY
3150
  CASE status
3151
  WHEN 'active' THEN 1
@@ -3156,7 +3387,7 @@ def handle_get_available_drivers(tool_input: dict) -> dict:
3156
  """
3157
 
3158
  try:
3159
- results = execute_query(query, (limit,))
3160
 
3161
  if not results:
3162
  return {
@@ -3216,7 +3447,7 @@ def handle_get_available_drivers(tool_input: dict) -> dict:
3216
  # ASSIGNMENT MANAGEMENT TOOLS
3217
  # ============================================================================
3218
 
3219
- def handle_create_assignment(tool_input: dict) -> dict:
3220
  """
3221
  Create assignment (assign order to driver)
3222
 
@@ -3225,10 +3456,22 @@ def handle_create_assignment(tool_input: dict) -> dict:
3225
 
3226
  Args:
3227
  tool_input: Dict with order_id and driver_id
 
3228
 
3229
  Returns:
3230
  Assignment creation result with route data
3231
  """
 
 
 
 
 
 
 
 
 
 
 
3232
  from datetime import datetime, timedelta
3233
 
3234
  order_id = (tool_input.get("order_id") or "").strip()
@@ -3246,12 +3489,12 @@ def handle_create_assignment(tool_input: dict) -> dict:
3246
  conn = get_db_connection()
3247
  cursor = conn.cursor()
3248
 
3249
- # Step 1: Validate order exists and status is "pending"
3250
  cursor.execute("""
3251
  SELECT status, delivery_lat, delivery_lng, delivery_address, assigned_driver_id
3252
  FROM orders
3253
- WHERE order_id = %s
3254
- """, (order_id,))
3255
 
3256
  order_row = cursor.fetchone()
3257
  if not order_row:
@@ -3299,12 +3542,12 @@ def handle_create_assignment(tool_input: dict) -> dict:
3299
  "error": "Order does not have delivery location coordinates"
3300
  }
3301
 
3302
- # Step 2: Validate driver exists and status is "active"
3303
  cursor.execute("""
3304
  SELECT status, current_lat, current_lng, vehicle_type, name
3305
  FROM drivers
3306
- WHERE driver_id = %s
3307
- """, (driver_id,))
3308
 
3309
  driver_row = cursor.fetchone()
3310
  if not driver_row:
@@ -3399,7 +3642,7 @@ def handle_create_assignment(tool_input: dict) -> dict:
3399
 
3400
  cursor.execute("""
3401
  INSERT INTO assignments (
3402
- assignment_id, order_id, driver_id,
3403
  route_distance_meters, route_duration_seconds, route_duration_in_traffic_seconds,
3404
  route_summary, route_confidence, route_directions,
3405
  driver_start_location_lat, driver_start_location_lng,
@@ -3407,7 +3650,7 @@ def handle_create_assignment(tool_input: dict) -> dict:
3407
  estimated_arrival, vehicle_type, traffic_delay_seconds,
3408
  status
3409
  ) VALUES (
3410
- %s, %s, %s,
3411
  %s, %s, %s,
3412
  %s, %s, %s,
3413
  %s, %s,
@@ -3416,7 +3659,7 @@ def handle_create_assignment(tool_input: dict) -> dict:
3416
  %s
3417
  )
3418
  """, (
3419
- assignment_id, order_id, driver_id,
3420
  route_result.get("distance", {}).get("meters", 0),
3421
  route_result.get("duration", {}).get("seconds", 0),
3422
  route_result.get("duration_in_traffic", {}).get("seconds", 0),
@@ -3477,7 +3720,7 @@ def handle_create_assignment(tool_input: dict) -> dict:
3477
  }
3478
 
3479
 
3480
- def handle_auto_assign_order(tool_input: dict) -> dict:
3481
  """
3482
  Automatically assign order to nearest available driver (distance + validation based).
3483
 
@@ -3489,10 +3732,22 @@ def handle_auto_assign_order(tool_input: dict) -> dict:
3489
 
3490
  Args:
3491
  tool_input: Dict with order_id
 
3492
 
3493
  Returns:
3494
  Assignment details with selected driver info and distance
3495
  """
 
 
 
 
 
 
 
 
 
 
 
3496
  order_id = (tool_input.get("order_id") or "").strip()
3497
 
3498
  if not order_id:
@@ -3505,7 +3760,7 @@ def handle_auto_assign_order(tool_input: dict) -> dict:
3505
  conn = get_db_connection()
3506
  cursor = conn.cursor(cursor_factory=RealDictCursor)
3507
 
3508
- # Step 1: Get order details with ALL requirements
3509
  cursor.execute("""
3510
  SELECT
3511
  order_id, customer_name, delivery_address,
@@ -3514,8 +3769,8 @@ def handle_auto_assign_order(tool_input: dict) -> dict:
3514
  requires_cold_storage, requires_signature,
3515
  priority, assigned_driver_id
3516
  FROM orders
3517
- WHERE order_id = %s
3518
- """, (order_id,))
3519
 
3520
  order = cursor.fetchone()
3521
 
@@ -3549,16 +3804,17 @@ def handle_auto_assign_order(tool_input: dict) -> dict:
3549
  needs_fragile_handling = order['is_fragile'] or False
3550
  needs_cold_storage = order['requires_cold_storage'] or False
3551
 
3552
- # Step 2: Get all active drivers with valid locations
3553
  cursor.execute("""
3554
  SELECT
3555
  driver_id, name, phone, current_lat, current_lng,
3556
  vehicle_type, capacity_kg, capacity_m3, skills
3557
  FROM drivers
3558
- WHERE status = 'active'
 
3559
  AND current_lat IS NOT NULL
3560
  AND current_lng IS NOT NULL
3561
- """)
3562
 
3563
  active_drivers = cursor.fetchall()
3564
 
@@ -3646,7 +3902,7 @@ def handle_auto_assign_order(tool_input: dict) -> dict:
3646
  assignment_result = handle_create_assignment({
3647
  "order_id": order_id,
3648
  "driver_id": selected_driver['driver_id']
3649
- })
3650
 
3651
  if not assignment_result.get("success"):
3652
  return assignment_result
@@ -3680,7 +3936,7 @@ def handle_auto_assign_order(tool_input: dict) -> dict:
3680
  }
3681
 
3682
 
3683
- def handle_intelligent_assign_order(tool_input: dict) -> dict:
3684
  """
3685
  Intelligently assign order using Gemini AI to analyze all parameters.
3686
 
@@ -3694,10 +3950,22 @@ def handle_intelligent_assign_order(tool_input: dict) -> dict:
3694
 
3695
  Args:
3696
  tool_input: Dict with order_id
 
3697
 
3698
  Returns:
3699
  Assignment details with AI reasoning and selected driver info
3700
  """
 
 
 
 
 
 
 
 
 
 
 
3701
  import os
3702
  import json
3703
  import google.generativeai as genai
@@ -3723,7 +3991,7 @@ def handle_intelligent_assign_order(tool_input: dict) -> dict:
3723
  conn = get_db_connection()
3724
  cursor = conn.cursor(cursor_factory=RealDictCursor)
3725
 
3726
- # Step 1: Get complete order details
3727
  cursor.execute("""
3728
  SELECT
3729
  order_id, customer_name, customer_phone, customer_email,
@@ -3735,8 +4003,8 @@ def handle_intelligent_assign_order(tool_input: dict) -> dict:
3735
  payment_status, special_instructions, status,
3736
  created_at, sla_grace_period_minutes
3737
  FROM orders
3738
- WHERE order_id = %s
3739
- """, (order_id,))
3740
 
3741
  order = cursor.fetchone()
3742
 
@@ -3764,7 +4032,7 @@ def handle_intelligent_assign_order(tool_input: dict) -> dict:
3764
  "error": "Order missing delivery coordinates. Cannot calculate routes."
3765
  }
3766
 
3767
- # Step 2: Get all active drivers with complete details
3768
  cursor.execute("""
3769
  SELECT
3770
  driver_id, name, phone, email,
@@ -3772,10 +4040,11 @@ def handle_intelligent_assign_order(tool_input: dict) -> dict:
3772
  vehicle_type, vehicle_plate, capacity_kg, capacity_m3,
3773
  skills, status, created_at, updated_at
3774
  FROM drivers
3775
- WHERE status = 'active'
 
3776
  AND current_lat IS NOT NULL
3777
  AND current_lng IS NOT NULL
3778
- """)
3779
 
3780
  active_drivers = cursor.fetchall()
3781
 
@@ -3966,7 +4235,7 @@ Analyze ALL parameters comprehensively:
3966
  assignment_result = handle_create_assignment({
3967
  "order_id": order_id,
3968
  "driver_id": selected_driver_id
3969
- })
3970
 
3971
  if not assignment_result.get("success"):
3972
  return assignment_result
@@ -4002,7 +4271,7 @@ Analyze ALL parameters comprehensively:
4002
  }
4003
 
4004
 
4005
- def handle_get_assignment_details(tool_input: dict) -> dict:
4006
  """
4007
  Get assignment details
4008
 
@@ -4011,10 +4280,22 @@ def handle_get_assignment_details(tool_input: dict) -> dict:
4011
 
4012
  Args:
4013
  tool_input: Dict with assignment_id, order_id, or driver_id
 
4014
 
4015
  Returns:
4016
  Assignment details or list of assignments
4017
  """
 
 
 
 
 
 
 
 
 
 
 
4018
  assignment_id = (tool_input.get("assignment_id") or "").strip()
4019
  order_id = (tool_input.get("order_id") or "").strip()
4020
  driver_id = (tool_input.get("driver_id") or "").strip()
@@ -4029,7 +4310,7 @@ def handle_get_assignment_details(tool_input: dict) -> dict:
4029
  conn = get_db_connection()
4030
  cursor = conn.cursor()
4031
 
4032
- # Build query based on provided parameters
4033
  query = """
4034
  SELECT
4035
  a.assignment_id, a.order_id, a.driver_id, a.status,
@@ -4044,10 +4325,10 @@ def handle_get_assignment_details(tool_input: dict) -> dict:
4044
  FROM assignments a
4045
  LEFT JOIN orders o ON a.order_id = o.order_id
4046
  LEFT JOIN drivers d ON a.driver_id = d.driver_id
4047
- WHERE 1=1
4048
  """
4049
 
4050
- params = []
4051
 
4052
  if assignment_id:
4053
  query += " AND a.assignment_id = %s"
@@ -4147,7 +4428,7 @@ def handle_get_assignment_details(tool_input: dict) -> dict:
4147
  }
4148
 
4149
 
4150
- def handle_update_assignment(tool_input: dict) -> dict:
4151
  """
4152
  Update assignment status
4153
 
@@ -4156,10 +4437,22 @@ def handle_update_assignment(tool_input: dict) -> dict:
4156
 
4157
  Args:
4158
  tool_input: Dict with assignment_id, status (optional), actual_arrival (optional), notes (optional)
 
4159
 
4160
  Returns:
4161
  Update result
4162
  """
 
 
 
 
 
 
 
 
 
 
 
4163
  from datetime import datetime
4164
 
4165
  assignment_id = (tool_input.get("assignment_id") or "").strip()
@@ -4193,12 +4486,12 @@ def handle_update_assignment(tool_input: dict) -> dict:
4193
  conn = get_db_connection()
4194
  cursor = conn.cursor()
4195
 
4196
- # Get current assignment details
4197
  cursor.execute("""
4198
  SELECT status, order_id, driver_id
4199
  FROM assignments
4200
- WHERE assignment_id = %s
4201
- """, (assignment_id,))
4202
 
4203
  assignment_row = cursor.fetchone()
4204
  if not assignment_row:
@@ -4332,7 +4625,7 @@ def handle_update_assignment(tool_input: dict) -> dict:
4332
  }
4333
 
4334
 
4335
- def handle_unassign_order(tool_input: dict) -> dict:
4336
  """
4337
  Unassign order (delete assignment)
4338
 
@@ -4340,10 +4633,22 @@ def handle_unassign_order(tool_input: dict) -> dict:
4340
 
4341
  Args:
4342
  tool_input: Dict with order_id or assignment_id, and confirm flag
 
4343
 
4344
  Returns:
4345
  Unassignment result
4346
  """
 
 
 
 
 
 
 
 
 
 
 
4347
  order_id = (tool_input.get("order_id") or "").strip()
4348
  assignment_id = (tool_input.get("assignment_id") or "").strip()
4349
  confirm = tool_input.get("confirm", False)
@@ -4366,21 +4671,21 @@ def handle_unassign_order(tool_input: dict) -> dict:
4366
  conn = get_db_connection()
4367
  cursor = conn.cursor()
4368
 
4369
- # Find assignment
4370
  if assignment_id:
4371
  cursor.execute("""
4372
  SELECT order_id, driver_id, status
4373
  FROM assignments
4374
- WHERE assignment_id = %s
4375
- """, (assignment_id,))
4376
  else:
4377
  cursor.execute("""
4378
  SELECT assignment_id, driver_id, status
4379
  FROM assignments
4380
- WHERE order_id = %s AND status IN ('active', 'in_progress')
4381
  ORDER BY assigned_at DESC
4382
  LIMIT 1
4383
- """, (order_id,))
4384
 
4385
  assignment_row = cursor.fetchone()
4386
  if not assignment_row:
@@ -4462,7 +4767,7 @@ def handle_unassign_order(tool_input: dict) -> dict:
4462
  }
4463
 
4464
 
4465
- def handle_complete_delivery(tool_input: dict) -> dict:
4466
  """
4467
  Complete a delivery and automatically update driver location
4468
 
@@ -4471,10 +4776,22 @@ def handle_complete_delivery(tool_input: dict) -> dict:
4471
 
4472
  Args:
4473
  tool_input: Dict with assignment_id, confirm flag, and optional fields
 
4474
 
4475
  Returns:
4476
  Completion result
4477
  """
 
 
 
 
 
 
 
 
 
 
 
4478
  from datetime import datetime
4479
 
4480
  assignment_id = (tool_input.get("assignment_id") or "").strip()
@@ -4500,7 +4817,7 @@ def handle_complete_delivery(tool_input: dict) -> dict:
4500
  conn = get_db_connection()
4501
  cursor = conn.cursor()
4502
 
4503
- # Get assignment and order details including timing fields
4504
  cursor.execute("""
4505
  SELECT
4506
  a.status, a.order_id, a.driver_id,
@@ -4510,8 +4827,8 @@ def handle_complete_delivery(tool_input: dict) -> dict:
4510
  FROM assignments a
4511
  JOIN orders o ON a.order_id = o.order_id
4512
  JOIN drivers d ON a.driver_id = d.driver_id
4513
- WHERE a.assignment_id = %s
4514
- """, (assignment_id,))
4515
 
4516
  assignment_row = cursor.fetchone()
4517
  if not assignment_row:
@@ -4684,7 +5001,7 @@ def handle_complete_delivery(tool_input: dict) -> dict:
4684
  }
4685
 
4686
 
4687
- def handle_fail_delivery(tool_input: dict) -> dict:
4688
  """
4689
  Mark delivery as failed with mandatory location and reason
4690
 
@@ -4694,10 +5011,22 @@ def handle_fail_delivery(tool_input: dict) -> dict:
4694
  Args:
4695
  tool_input: Dict with assignment_id, current_lat, current_lng, failure_reason,
4696
  confirm flag, and optional notes
 
4697
 
4698
  Returns:
4699
  Failure recording result
4700
  """
 
 
 
 
 
 
 
 
 
 
 
4701
  from datetime import datetime
4702
 
4703
  assignment_id = (tool_input.get("assignment_id") or "").strip()
@@ -4772,7 +5101,7 @@ def handle_fail_delivery(tool_input: dict) -> dict:
4772
  conn = get_db_connection()
4773
  cursor = conn.cursor()
4774
 
4775
- # Get assignment and order details including timing fields
4776
  cursor.execute("""
4777
  SELECT
4778
  a.status, a.order_id, a.driver_id,
@@ -4782,8 +5111,8 @@ def handle_fail_delivery(tool_input: dict) -> dict:
4782
  FROM assignments a
4783
  JOIN orders o ON a.order_id = o.order_id
4784
  JOIN drivers d ON a.driver_id = d.driver_id
4785
- WHERE a.assignment_id = %s
4786
- """, (assignment_id,))
4787
 
4788
  assignment_row = cursor.fetchone()
4789
  if not assignment_row:
 
3
  Simulates MCP tools using Claude's tool calling feature
4
  """
5
 
6
+ import os
7
  import sys
8
  from pathlib import Path
9
  from datetime import datetime, timedelta
 
1329
  }
1330
 
1331
 
1332
+ def handle_create_order(tool_input: dict, user_id: str = None) -> dict:
1333
  """
1334
  Execute order creation tool
1335
 
1336
  Args:
1337
  tool_input: Dict with order fields (expected_delivery_time now REQUIRED)
1338
+ user_id: ID of authenticated user creating the order
1339
 
1340
  Returns:
1341
  Order creation result
1342
  """
1343
+ # Authentication check - allow dev mode
1344
+ if not user_id:
1345
+ # Development mode - use default user
1346
+ if os.getenv("SKIP_AUTH") == "true":
1347
+ user_id = "dev-user"
1348
+ else:
1349
+ return {
1350
+ "success": False,
1351
+ "error": "Authentication required. Please login first.",
1352
+ "auth_required": True
1353
+ }
1354
+
1355
  # Extract fields with defaults
1356
  customer_name = tool_input.get("customer_name")
1357
  customer_phone = tool_input.get("customer_phone")
 
1415
  delivery_address, delivery_lat, delivery_lng,
1416
  time_window_start, time_window_end, expected_delivery_time,
1417
  priority, weight_kg, volume_m3, is_fragile, requires_cold_storage, requires_signature,
1418
+ status, special_instructions, sla_grace_period_minutes, user_id
1419
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
1420
  """
1421
 
1422
  params = (
 
1438
  requires_signature,
1439
  "pending",
1440
  special_instructions,
1441
+ sla_grace_period_minutes,
1442
+ user_id # Add user_id to track ownership
1443
  )
1444
 
1445
  try:
 
1465
  }
1466
 
1467
 
1468
+ def handle_create_driver(tool_input: dict, user_id: str = None) -> dict:
1469
  """
1470
  Execute driver creation tool
1471
 
1472
  Args:
1473
  tool_input: Dict with driver fields
1474
+ user_id: ID of authenticated user creating the driver
1475
 
1476
  Returns:
1477
  Driver creation result
1478
  """
1479
+ # Authentication check - allow dev mode
1480
+ if not user_id:
1481
+ if os.getenv("SKIP_AUTH") == "true":
1482
+ user_id = "dev-user"
1483
+ else:
1484
+ return {
1485
+ "success": False,
1486
+ "error": "Authentication required. Please login first.",
1487
+ "auth_required": True
1488
+ }
1489
+
1490
  # Extract fields with defaults
1491
  name = tool_input.get("name")
1492
  phone = tool_input.get("phone")
 
1544
  driver_id, name, phone, email,
1545
  current_lat, current_lng, last_location_update,
1546
  status, vehicle_type, vehicle_plate,
1547
+ capacity_kg, capacity_m3, skills, user_id
1548
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
1549
  """
1550
 
1551
  # Convert skills list to JSON
 
1565
  vehicle_plate,
1566
  capacity_kg,
1567
  capacity_m3,
1568
+ skills_json,
1569
+ user_id # Add user_id to track ownership
1570
  )
1571
 
1572
  try:
 
1592
  }
1593
 
1594
 
1595
+ def handle_update_order(tool_input: dict, user_id: str = None) -> dict:
1596
  """
1597
  Execute order update tool with assignment cascading logic
1598
 
1599
  Args:
1600
  tool_input: Dict with order_id and fields to update
1601
+ user_id: Authenticated user ID
1602
 
1603
  Returns:
1604
  Update result
1605
  """
1606
+ # Authentication check - allow dev mode
1607
+ if not user_id:
1608
+ if os.getenv("SKIP_AUTH") == "true":
1609
+ user_id = "dev-user"
1610
+ else:
1611
+ return {
1612
+ "success": False,
1613
+ "error": "Authentication required. Please login first.",
1614
+ "auth_required": True
1615
+ }
1616
+
1617
  import json
1618
 
1619
  order_id = tool_input.get("order_id")
 
1625
  "error": "Missing required field: order_id"
1626
  }
1627
 
1628
+ # Check if order exists and belongs to user
1629
+ check_query = "SELECT order_id, status, assigned_driver_id FROM orders WHERE order_id = %s AND user_id = %s"
1630
+ existing = execute_query(check_query, (order_id, user_id))
1631
 
1632
  if not existing:
1633
  return {
 
1784
  update_fields.append("updated_at = %s")
1785
  params.append(datetime.now())
1786
 
1787
+ # Add order_id and user_id for WHERE clause
1788
  params.append(order_id)
1789
+ params.append(user_id)
1790
 
1791
  # Execute update
1792
  query = f"""
1793
  UPDATE orders
1794
  SET {', '.join(update_fields)}
1795
+ WHERE order_id = %s AND user_id = %s
1796
  """
1797
 
1798
  try:
 
1818
  }
1819
 
1820
 
1821
+ def handle_delete_all_orders(tool_input: dict, user_id: str = None) -> dict:
1822
  """
1823
+ Delete all orders (bulk delete) for the authenticated user
1824
 
1825
  Args:
1826
  tool_input: Dict with confirm flag and optional status filter
1827
+ user_id: Authenticated user ID
1828
 
1829
  Returns:
1830
  Deletion result with count
1831
  """
1832
+ # Authentication check - allow dev mode
1833
+ if not user_id:
1834
+ if os.getenv("SKIP_AUTH") == "true":
1835
+ user_id = "dev-user"
1836
+ else:
1837
+ return {
1838
+ "success": False,
1839
+ "error": "Authentication required. Please login first.",
1840
+ "auth_required": True
1841
+ }
1842
+
1843
  confirm = tool_input.get("confirm", False)
1844
  status_filter = tool_input.get("status") # Optional: delete only specific status
1845
 
 
1850
  }
1851
 
1852
  try:
1853
+ # Check for active assignments first (only for this user's orders)
1854
  active_assignments = execute_query("""
1855
  SELECT COUNT(*) as count FROM assignments
1856
+ WHERE user_id = %s AND status IN ('active', 'in_progress')
1857
+ """, (user_id,))
1858
 
1859
  active_count = active_assignments[0]['count']
1860
 
 
1864
  "error": f"Cannot delete orders: {active_count} active assignment(s) exist. Cancel or complete them first."
1865
  }
1866
 
1867
+ # Build delete query based on status filter (always filter by user_id)
1868
  if status_filter:
1869
+ count_query = "SELECT COUNT(*) as count FROM orders WHERE user_id = %s AND status = %s"
1870
+ delete_query = "DELETE FROM orders WHERE user_id = %s AND status = %s"
1871
+ params = (user_id, status_filter)
1872
  else:
1873
+ count_query = "SELECT COUNT(*) as count FROM orders WHERE user_id = %s"
1874
+ delete_query = "DELETE FROM orders WHERE user_id = %s"
1875
+ params = (user_id,)
1876
 
1877
  # Get count before deletion
1878
  count_result = execute_query(count_query, params)
 
1903
  }
1904
 
1905
 
1906
+ def handle_delete_order(tool_input: dict, user_id: str = None) -> dict:
1907
  """
1908
  Execute order deletion tool with assignment safety checks
1909
 
1910
  Args:
1911
  tool_input: Dict with order_id and confirm flag
1912
+ user_id: Authenticated user ID
1913
 
1914
  Returns:
1915
  Deletion result
1916
  """
1917
+ # Authentication check - allow dev mode
1918
+ if not user_id:
1919
+ if os.getenv("SKIP_AUTH") == "true":
1920
+ user_id = "dev-user"
1921
+ else:
1922
+ return {
1923
+ "success": False,
1924
+ "error": "Authentication required. Please login first.",
1925
+ "auth_required": True
1926
+ }
1927
+
1928
  order_id = tool_input.get("order_id")
1929
  confirm = tool_input.get("confirm", False)
1930
 
 
1941
  "error": "Deletion not confirmed. Set confirm=true to proceed."
1942
  }
1943
 
1944
+ # Check if order exists and belongs to user
1945
+ check_query = "SELECT order_id, status FROM orders WHERE order_id = %s AND user_id = %s"
1946
+ existing = execute_query(check_query, (order_id, user_id))
1947
 
1948
  if not existing:
1949
  return {
 
1982
  cascading_info.append(f"{completed_assignments[0]['count']} completed/failed/cancelled assignment(s) will be cascade deleted")
1983
 
1984
  # Delete the order (will cascade to assignments via FK)
1985
+ query = "DELETE FROM orders WHERE order_id = %s AND user_id = %s"
1986
 
1987
  try:
1988
+ execute_write(query, (order_id, user_id))
1989
  logger.info(f"Order deleted: {order_id}")
1990
 
1991
  result = {
 
2006
  }
2007
 
2008
 
2009
+ def handle_update_driver(tool_input: dict, user_id: str = None) -> dict:
2010
  """
2011
  Execute driver update tool with assignment validation
2012
 
2013
  Args:
2014
  tool_input: Dict with driver_id and fields to update
2015
+ user_id: Authenticated user ID
2016
 
2017
  Returns:
2018
  Update result
2019
  """
2020
+ # Authentication check - allow dev mode
2021
+ if not user_id:
2022
+ if os.getenv("SKIP_AUTH") == "true":
2023
+ user_id = "dev-user"
2024
+ else:
2025
+ return {
2026
+ "success": False,
2027
+ "error": "Authentication required. Please login first.",
2028
+ "auth_required": True
2029
+ }
2030
+
2031
  import json
2032
 
2033
  driver_id = tool_input.get("driver_id")
 
2039
  "error": "Missing required field: driver_id"
2040
  }
2041
 
2042
+ # Check if driver exists and belongs to user
2043
+ check_query = "SELECT driver_id, status FROM drivers WHERE driver_id = %s AND user_id = %s"
2044
+ existing = execute_query(check_query, (driver_id, user_id))
2045
 
2046
  if not existing:
2047
  return {
 
2122
  update_fields.append("last_location_update = %s")
2123
  params.append(datetime.now())
2124
 
2125
+ # Add driver_id and user_id for WHERE clause
2126
  params.append(driver_id)
2127
+ params.append(user_id)
2128
 
2129
  # Execute update
2130
  query = f"""
2131
  UPDATE drivers
2132
  SET {', '.join(update_fields)}
2133
+ WHERE driver_id = %s AND user_id = %s
2134
  """
2135
 
2136
  try:
 
2155
  }
2156
 
2157
 
2158
+ def handle_delete_all_drivers(tool_input: dict, user_id: str = None) -> dict:
2159
  """
2160
+ Delete all drivers (bulk delete) for the authenticated user
2161
 
2162
  Args:
2163
  tool_input: Dict with confirm flag and optional status filter
2164
+ user_id: Authenticated user ID
2165
 
2166
  Returns:
2167
  Deletion result with count
2168
  """
2169
+ # Authentication check - allow dev mode
2170
+ if not user_id:
2171
+ if os.getenv("SKIP_AUTH") == "true":
2172
+ user_id = "dev-user"
2173
+ else:
2174
+ return {
2175
+ "success": False,
2176
+ "error": "Authentication required. Please login first.",
2177
+ "auth_required": True
2178
+ }
2179
+
2180
  confirm = tool_input.get("confirm", False)
2181
  status_filter = tool_input.get("status") # Optional: delete only specific status
2182
 
 
2187
  }
2188
 
2189
  try:
2190
+ # Check for ANY assignments for this user (RESTRICT constraint will block if any exist)
2191
  assignments = execute_query("""
2192
+ SELECT COUNT(*) as count FROM assignments WHERE user_id = %s
2193
+ """, (user_id,))
2194
 
2195
  assignment_count = assignments[0]['count']
2196
 
 
2200
  "error": f"Cannot delete drivers: {assignment_count} assignment(s) exist in database. Database RESTRICT constraint prevents driver deletion when assignments exist."
2201
  }
2202
 
2203
+ # Build delete query based on status filter (always filter by user_id)
2204
  if status_filter:
2205
+ count_query = "SELECT COUNT(*) as count FROM drivers WHERE user_id = %s AND status = %s"
2206
+ delete_query = "DELETE FROM drivers WHERE user_id = %s AND status = %s"
2207
+ params = (user_id, status_filter)
2208
  else:
2209
+ count_query = "SELECT COUNT(*) as count FROM drivers WHERE user_id = %s"
2210
+ delete_query = "DELETE FROM drivers WHERE user_id = %s"
2211
+ params = (user_id,)
2212
 
2213
  # Get count before deletion
2214
  count_result = execute_query(count_query, params)
 
2245
  }
2246
 
2247
 
2248
+ def handle_delete_driver(tool_input: dict, user_id: str = None) -> dict:
2249
  """
2250
  Execute driver deletion tool with assignment safety checks
2251
 
2252
  Args:
2253
  tool_input: Dict with driver_id and confirm flag
2254
+ user_id: Authenticated user ID
2255
 
2256
  Returns:
2257
  Deletion result
2258
  """
2259
+ # Authentication check - allow dev mode
2260
+ if not user_id:
2261
+ if os.getenv("SKIP_AUTH") == "true":
2262
+ user_id = "dev-user"
2263
+ else:
2264
+ return {
2265
+ "success": False,
2266
+ "error": "Authentication required. Please login first.",
2267
+ "auth_required": True
2268
+ }
2269
+
2270
  driver_id = tool_input.get("driver_id")
2271
  confirm = tool_input.get("confirm", False)
2272
 
 
2283
  "error": "Deletion not confirmed. Set confirm=true to proceed."
2284
  }
2285
 
2286
+ # Check if driver exists and belongs to user
2287
+ check_query = "SELECT driver_id, name FROM drivers WHERE driver_id = %s AND user_id = %s"
2288
+ existing = execute_query(check_query, (driver_id, user_id))
2289
 
2290
  if not existing:
2291
  return {
 
2343
  cascading_info.append(f"{order_count} order(s) will have assigned_driver_id set to NULL")
2344
 
2345
  # Delete the driver
2346
+ query = "DELETE FROM drivers WHERE driver_id = %s AND user_id = %s"
2347
 
2348
  try:
2349
+ execute_write(query, (driver_id, user_id))
2350
  logger.info(f"Driver deleted: {driver_id}")
2351
 
2352
  result = {
 
2373
  }
2374
 
2375
 
2376
+ def handle_count_orders(tool_input: dict, user_id: str = None) -> dict:
2377
  """
2378
  Execute count orders tool
2379
 
2380
  Args:
2381
  tool_input: Dict with optional filter fields
2382
+ user_id: ID of authenticated user
2383
 
2384
  Returns:
2385
+ Order count result with breakdown (only user's orders)
2386
  """
2387
+ # Authentication check - allow dev mode
2388
+ if not user_id:
2389
+ if os.getenv("SKIP_AUTH") == "true":
2390
+ user_id = "dev-user"
2391
+ else:
2392
+ return {
2393
+ "success": False,
2394
+ "error": "Authentication required. Please login first.",
2395
+ "auth_required": True
2396
+ }
2397
+
2398
  # Build WHERE clause based on filters
2399
+ # IMPORTANT: Always filter by user_id FIRST
2400
+ where_clauses = ["user_id = %s"]
2401
+ params = [user_id]
2402
 
2403
  if "status" in tool_input:
2404
  where_clauses.append("status = %s")
 
2482
  }
2483
 
2484
 
2485
+ def handle_fetch_orders(tool_input: dict, user_id: str = None) -> dict:
2486
  """
2487
  Execute fetch orders tool
2488
 
2489
  Args:
2490
  tool_input: Dict with filter, pagination, and sorting options
2491
+ user_id: ID of authenticated user (filters to only their orders)
2492
 
2493
  Returns:
2494
+ List of orders matching criteria (only user's orders)
2495
  """
2496
+ # Authentication check - allow dev mode
2497
+ if not user_id:
2498
+ # Development mode - use default user
2499
+ if os.getenv("SKIP_AUTH") == "true":
2500
+ user_id = "dev-user"
2501
+ else:
2502
+ return {
2503
+ "success": False,
2504
+ "error": "Authentication required. Please login first.",
2505
+ "auth_required": True
2506
+ }
2507
+
2508
  # Extract pagination and sorting
2509
  limit = min(tool_input.get("limit", 10), 100) # Cap at 100
2510
  offset = tool_input.get("offset", 0)
 
2512
  sort_order = tool_input.get("sort_order", "DESC")
2513
 
2514
  # Build WHERE clause based on filters
2515
+ # IMPORTANT: Always filter by user_id FIRST for security
2516
+ where_clauses = ["user_id = %s"]
2517
+ params = [user_id]
2518
 
2519
  if "status" in tool_input:
2520
  where_clauses.append("status = %s")
 
2636
  }
2637
 
2638
 
2639
+ def handle_get_order_details(tool_input: dict, user_id: str = None) -> dict:
2640
  """
2641
  Execute get order details tool
2642
 
2643
  Args:
2644
  tool_input: Dict with order_id
2645
+ user_id: ID of authenticated user
2646
 
2647
  Returns:
2648
+ Complete order details (only if owned by user)
2649
  """
2650
+ # Authentication check - allow dev mode
2651
+ if not user_id:
2652
+ if os.getenv("SKIP_AUTH") == "true":
2653
+ user_id = "dev-user"
2654
+ else:
2655
+ return {
2656
+ "success": False,
2657
+ "error": "Authentication required. Please login first.",
2658
+ "auth_required": True
2659
+ }
2660
+
2661
  order_id = tool_input.get("order_id")
2662
 
2663
  if not order_id:
 
2678
  order_value, payment_status,
2679
  requires_signature, is_fragile, requires_cold_storage
2680
  FROM orders
2681
+ WHERE order_id = %s AND user_id = %s
2682
  """
2683
 
2684
  try:
2685
+ results = execute_query(query, (order_id, user_id))
2686
 
2687
  if not results:
2688
  return {
 
2757
  }
2758
 
2759
 
2760
+ def handle_search_orders(tool_input: dict, user_id: str = None) -> dict:
2761
  """
2762
  Execute search orders tool
2763
 
2764
  Args:
2765
  tool_input: Dict with search_term
2766
+ user_id: Authenticated user ID
2767
 
2768
  Returns:
2769
  List of matching orders
2770
  """
2771
+ # Authentication check - allow dev mode
2772
+ if not user_id:
2773
+ if os.getenv("SKIP_AUTH") == "true":
2774
+ user_id = "dev-user"
2775
+ else:
2776
+ return {
2777
+ "success": False,
2778
+ "error": "Authentication required. Please login first.",
2779
+ "auth_required": True
2780
+ }
2781
+
2782
  search_term = tool_input.get("search_term", "").strip()
2783
 
2784
  if not search_term:
 
2793
  delivery_address, priority, status, created_at
2794
  FROM orders
2795
  WHERE
2796
+ user_id = %s AND (
2797
+ order_id ILIKE %s OR
2798
+ customer_name ILIKE %s OR
2799
+ customer_email ILIKE %s OR
2800
+ customer_phone ILIKE %s
2801
+ )
2802
  ORDER BY created_at DESC
2803
  LIMIT 50
2804
  """
2805
 
2806
  search_pattern = f"%{search_term}%"
2807
+ params = (user_id, search_pattern, search_pattern, search_pattern, search_pattern)
2808
 
2809
  try:
2810
  results = execute_query(query, params)
 
2846
  }
2847
 
2848
 
2849
+ def handle_get_incomplete_orders(tool_input: dict, user_id: str = None) -> dict:
2850
  """
2851
  Execute get incomplete orders tool
2852
 
2853
  Args:
2854
  tool_input: Dict with optional limit
2855
+ user_id: Authenticated user ID
2856
 
2857
  Returns:
2858
  List of incomplete orders (pending, assigned, in_transit)
2859
  """
2860
+ # Authentication check - allow dev mode
2861
+ if not user_id:
2862
+ if os.getenv("SKIP_AUTH") == "true":
2863
+ user_id = "dev-user"
2864
+ else:
2865
+ return {
2866
+ "success": False,
2867
+ "error": "Authentication required. Please login first.",
2868
+ "auth_required": True
2869
+ }
2870
+
2871
  limit = min(tool_input.get("limit", 20), 100)
2872
 
2873
  query = """
 
2876
  priority, status, time_window_end, created_at,
2877
  assigned_driver_id
2878
  FROM orders
2879
+ WHERE user_id = %s AND status IN ('pending', 'assigned', 'in_transit')
2880
  ORDER BY
2881
  CASE priority
2882
  WHEN 'urgent' THEN 1
 
2888
  """
2889
 
2890
  try:
2891
+ results = execute_query(query, (user_id, limit))
2892
 
2893
  if not results:
2894
  return {
 
2927
  }
2928
 
2929
 
2930
+ def handle_count_drivers(tool_input: dict, user_id: str = None) -> dict:
2931
  """
2932
  Execute count drivers tool
2933
 
2934
  Args:
2935
  tool_input: Dict with optional filter fields
2936
+ user_id: Authenticated user ID
2937
 
2938
  Returns:
2939
  Driver count result with breakdown
2940
  """
2941
+ # Authentication check - allow dev mode
2942
+ if not user_id:
2943
+ if os.getenv("SKIP_AUTH") == "true":
2944
+ user_id = "dev-user"
2945
+ else:
2946
+ return {
2947
+ "success": False,
2948
+ "error": "Authentication required. Please login first.",
2949
+ "auth_required": True
2950
+ }
2951
+
2952
  # Build WHERE clause based on filters
2953
+ # IMPORTANT: Always filter by user_id FIRST
2954
+ where_clauses = ["user_id = %s"]
2955
+ params = [user_id]
2956
 
2957
  if "status" in tool_input:
2958
  where_clauses.append("status = %s")
 
3012
  }
3013
 
3014
 
3015
+ def handle_fetch_drivers(tool_input: dict, user_id: str = None) -> dict:
3016
  """
3017
  Execute fetch drivers tool
3018
 
3019
  Args:
3020
  tool_input: Dict with filter, pagination, and sorting options
3021
+ user_id: ID of authenticated user (filters to only their drivers)
3022
 
3023
  Returns:
3024
+ List of drivers matching criteria (only user's drivers)
3025
  """
3026
+ # Authentication check - allow dev mode
3027
+ if not user_id:
3028
+ if os.getenv("SKIP_AUTH") == "true":
3029
+ user_id = "dev-user"
3030
+ else:
3031
+ return {
3032
+ "success": False,
3033
+ "error": "Authentication required. Please login first.",
3034
+ "auth_required": True
3035
+ }
3036
+
3037
  # Extract pagination and sorting
3038
  limit = min(tool_input.get("limit", 10), 100) # Cap at 100
3039
  offset = tool_input.get("offset", 0)
 
3041
  sort_order = tool_input.get("sort_order", "ASC")
3042
 
3043
  # Build WHERE clause based on filters
3044
+ # IMPORTANT: Always filter by user_id FIRST for security
3045
+ where_clauses = ["user_id = %s"]
3046
+ params = [user_id]
3047
 
3048
  if "status" in tool_input:
3049
  where_clauses.append("status = %s")
 
3137
  }
3138
 
3139
 
3140
+ def handle_get_driver_details(tool_input: dict, user_id: str = None) -> dict:
3141
  """
3142
  Execute get driver details tool
3143
 
3144
  Args:
3145
  tool_input: Dict with driver_id
3146
+ user_id: Authenticated user ID
3147
 
3148
  Returns:
3149
  Complete driver details
3150
  """
3151
+ # Authentication check - allow dev mode
3152
+ if not user_id:
3153
+ if os.getenv("SKIP_AUTH") == "true":
3154
+ user_id = "dev-user"
3155
+ else:
3156
+ return {
3157
+ "success": False,
3158
+ "error": "Authentication required. Please login first.",
3159
+ "auth_required": True
3160
+ }
3161
+
3162
  driver_id = tool_input.get("driver_id")
3163
 
3164
  if not driver_id:
 
3175
  capacity_kg, capacity_m3, skills,
3176
  created_at, updated_at
3177
  FROM drivers
3178
+ WHERE driver_id = %s AND user_id = %s
3179
  """
3180
 
3181
  try:
3182
+ results = execute_query(query, (driver_id, user_id))
3183
 
3184
  if not results:
3185
  return {
 
3256
  }
3257
 
3258
 
3259
+ def handle_search_drivers(tool_input: dict, user_id: str = None) -> dict:
3260
  """
3261
  Execute search drivers tool
3262
 
3263
  Args:
3264
  tool_input: Dict with search_term
3265
+ user_id: Authenticated user ID
3266
 
3267
  Returns:
3268
  List of matching drivers
3269
  """
3270
+ # Authentication check - allow dev mode
3271
+ if not user_id:
3272
+ if os.getenv("SKIP_AUTH") == "true":
3273
+ user_id = "dev-user"
3274
+ else:
3275
+ return {
3276
+ "success": False,
3277
+ "error": "Authentication required. Please login first.",
3278
+ "auth_required": True
3279
+ }
3280
+
3281
  search_term = tool_input.get("search_term", "").strip()
3282
 
3283
  if not search_term:
 
3292
  vehicle_type, vehicle_plate, status, created_at
3293
  FROM drivers
3294
  WHERE
3295
+ user_id = %s AND (
3296
+ driver_id ILIKE %s OR
3297
+ name ILIKE %s OR
3298
+ email ILIKE %s OR
3299
+ phone ILIKE %s OR
3300
+ vehicle_plate ILIKE %s
3301
+ )
3302
  ORDER BY name ASC
3303
  LIMIT 50
3304
  """
3305
 
3306
  search_pattern = f"%{search_term}%"
3307
+ params = (user_id, search_pattern, search_pattern, search_pattern, search_pattern, search_pattern)
3308
 
3309
  try:
3310
  results = execute_query(query, params)
 
3346
  }
3347
 
3348
 
3349
+ def handle_get_available_drivers(tool_input: dict, user_id: str = None) -> dict:
3350
  """
3351
  Execute get available drivers tool
3352
 
3353
  Args:
3354
  tool_input: Dict with optional limit
3355
+ user_id: Authenticated user ID
3356
 
3357
  Returns:
3358
  List of available drivers (active or offline)
3359
  """
3360
+ # Authentication check - allow dev mode
3361
+ if not user_id:
3362
+ if os.getenv("SKIP_AUTH") == "true":
3363
+ user_id = "dev-user"
3364
+ else:
3365
+ return {
3366
+ "success": False,
3367
+ "error": "Authentication required. Please login first.",
3368
+ "auth_required": True
3369
+ }
3370
+
3371
  limit = min(tool_input.get("limit", 20), 100)
3372
 
3373
  query = """
 
3376
  current_lat, current_lng, last_location_update,
3377
  status, capacity_kg, capacity_m3, skills
3378
  FROM drivers
3379
+ WHERE user_id = %s AND status IN ('active', 'offline')
3380
  ORDER BY
3381
  CASE status
3382
  WHEN 'active' THEN 1
 
3387
  """
3388
 
3389
  try:
3390
+ results = execute_query(query, (user_id, limit))
3391
 
3392
  if not results:
3393
  return {
 
3447
  # ASSIGNMENT MANAGEMENT TOOLS
3448
  # ============================================================================
3449
 
3450
+ def handle_create_assignment(tool_input: dict, user_id: str = None) -> dict:
3451
  """
3452
  Create assignment (assign order to driver)
3453
 
 
3456
 
3457
  Args:
3458
  tool_input: Dict with order_id and driver_id
3459
+ user_id: Authenticated user ID
3460
 
3461
  Returns:
3462
  Assignment creation result with route data
3463
  """
3464
+ # Authentication check - allow dev mode
3465
+ if not user_id:
3466
+ if os.getenv("SKIP_AUTH") == "true":
3467
+ user_id = "dev-user"
3468
+ else:
3469
+ return {
3470
+ "success": False,
3471
+ "error": "Authentication required. Please login first.",
3472
+ "auth_required": True
3473
+ }
3474
+
3475
  from datetime import datetime, timedelta
3476
 
3477
  order_id = (tool_input.get("order_id") or "").strip()
 
3489
  conn = get_db_connection()
3490
  cursor = conn.cursor()
3491
 
3492
+ # Step 1: Validate order exists, belongs to user, and status is "pending"
3493
  cursor.execute("""
3494
  SELECT status, delivery_lat, delivery_lng, delivery_address, assigned_driver_id
3495
  FROM orders
3496
+ WHERE order_id = %s AND user_id = %s
3497
+ """, (order_id, user_id))
3498
 
3499
  order_row = cursor.fetchone()
3500
  if not order_row:
 
3542
  "error": "Order does not have delivery location coordinates"
3543
  }
3544
 
3545
+ # Step 2: Validate driver exists, belongs to user, and status is "active"
3546
  cursor.execute("""
3547
  SELECT status, current_lat, current_lng, vehicle_type, name
3548
  FROM drivers
3549
+ WHERE driver_id = %s AND user_id = %s
3550
+ """, (driver_id, user_id))
3551
 
3552
  driver_row = cursor.fetchone()
3553
  if not driver_row:
 
3642
 
3643
  cursor.execute("""
3644
  INSERT INTO assignments (
3645
+ assignment_id, order_id, driver_id, user_id,
3646
  route_distance_meters, route_duration_seconds, route_duration_in_traffic_seconds,
3647
  route_summary, route_confidence, route_directions,
3648
  driver_start_location_lat, driver_start_location_lng,
 
3650
  estimated_arrival, vehicle_type, traffic_delay_seconds,
3651
  status
3652
  ) VALUES (
3653
+ %s, %s, %s, %s,
3654
  %s, %s, %s,
3655
  %s, %s, %s,
3656
  %s, %s,
 
3659
  %s
3660
  )
3661
  """, (
3662
+ assignment_id, order_id, driver_id, user_id,
3663
  route_result.get("distance", {}).get("meters", 0),
3664
  route_result.get("duration", {}).get("seconds", 0),
3665
  route_result.get("duration_in_traffic", {}).get("seconds", 0),
 
3720
  }
3721
 
3722
 
3723
+ def handle_auto_assign_order(tool_input: dict, user_id: str = None) -> dict:
3724
  """
3725
  Automatically assign order to nearest available driver (distance + validation based).
3726
 
 
3732
 
3733
  Args:
3734
  tool_input: Dict with order_id
3735
+ user_id: Authenticated user ID
3736
 
3737
  Returns:
3738
  Assignment details with selected driver info and distance
3739
  """
3740
+ # Authentication check - allow dev mode
3741
+ if not user_id:
3742
+ if os.getenv("SKIP_AUTH") == "true":
3743
+ user_id = "dev-user"
3744
+ else:
3745
+ return {
3746
+ "success": False,
3747
+ "error": "Authentication required. Please login first.",
3748
+ "auth_required": True
3749
+ }
3750
+
3751
  order_id = (tool_input.get("order_id") or "").strip()
3752
 
3753
  if not order_id:
 
3760
  conn = get_db_connection()
3761
  cursor = conn.cursor(cursor_factory=RealDictCursor)
3762
 
3763
+ # Step 1: Get order details with ALL requirements (filtered by user_id)
3764
  cursor.execute("""
3765
  SELECT
3766
  order_id, customer_name, delivery_address,
 
3769
  requires_cold_storage, requires_signature,
3770
  priority, assigned_driver_id
3771
  FROM orders
3772
+ WHERE order_id = %s AND user_id = %s
3773
+ """, (order_id, user_id))
3774
 
3775
  order = cursor.fetchone()
3776
 
 
3804
  needs_fragile_handling = order['is_fragile'] or False
3805
  needs_cold_storage = order['requires_cold_storage'] or False
3806
 
3807
+ # Step 2: Get all active drivers with valid locations (filtered by user_id)
3808
  cursor.execute("""
3809
  SELECT
3810
  driver_id, name, phone, current_lat, current_lng,
3811
  vehicle_type, capacity_kg, capacity_m3, skills
3812
  FROM drivers
3813
+ WHERE user_id = %s
3814
+ AND status = 'active'
3815
  AND current_lat IS NOT NULL
3816
  AND current_lng IS NOT NULL
3817
+ """, (user_id,))
3818
 
3819
  active_drivers = cursor.fetchall()
3820
 
 
3902
  assignment_result = handle_create_assignment({
3903
  "order_id": order_id,
3904
  "driver_id": selected_driver['driver_id']
3905
+ }, user_id=user_id)
3906
 
3907
  if not assignment_result.get("success"):
3908
  return assignment_result
 
3936
  }
3937
 
3938
 
3939
+ def handle_intelligent_assign_order(tool_input: dict, user_id: str = None) -> dict:
3940
  """
3941
  Intelligently assign order using Gemini AI to analyze all parameters.
3942
 
 
3950
 
3951
  Args:
3952
  tool_input: Dict with order_id
3953
+ user_id: Authenticated user ID
3954
 
3955
  Returns:
3956
  Assignment details with AI reasoning and selected driver info
3957
  """
3958
+ # Authentication check - allow dev mode
3959
+ if not user_id:
3960
+ if os.getenv("SKIP_AUTH") == "true":
3961
+ user_id = "dev-user"
3962
+ else:
3963
+ return {
3964
+ "success": False,
3965
+ "error": "Authentication required. Please login first.",
3966
+ "auth_required": True
3967
+ }
3968
+
3969
  import os
3970
  import json
3971
  import google.generativeai as genai
 
3991
  conn = get_db_connection()
3992
  cursor = conn.cursor(cursor_factory=RealDictCursor)
3993
 
3994
+ # Step 1: Get complete order details (filtered by user_id)
3995
  cursor.execute("""
3996
  SELECT
3997
  order_id, customer_name, customer_phone, customer_email,
 
4003
  payment_status, special_instructions, status,
4004
  created_at, sla_grace_period_minutes
4005
  FROM orders
4006
+ WHERE order_id = %s AND user_id = %s
4007
+ """, (order_id, user_id))
4008
 
4009
  order = cursor.fetchone()
4010
 
 
4032
  "error": "Order missing delivery coordinates. Cannot calculate routes."
4033
  }
4034
 
4035
+ # Step 2: Get all active drivers with complete details (filtered by user_id)
4036
  cursor.execute("""
4037
  SELECT
4038
  driver_id, name, phone, email,
 
4040
  vehicle_type, vehicle_plate, capacity_kg, capacity_m3,
4041
  skills, status, created_at, updated_at
4042
  FROM drivers
4043
+ WHERE user_id = %s
4044
+ AND status = 'active'
4045
  AND current_lat IS NOT NULL
4046
  AND current_lng IS NOT NULL
4047
+ """, (user_id,))
4048
 
4049
  active_drivers = cursor.fetchall()
4050
 
 
4235
  assignment_result = handle_create_assignment({
4236
  "order_id": order_id,
4237
  "driver_id": selected_driver_id
4238
+ }, user_id=user_id)
4239
 
4240
  if not assignment_result.get("success"):
4241
  return assignment_result
 
4271
  }
4272
 
4273
 
4274
+ def handle_get_assignment_details(tool_input: dict, user_id: str = None) -> dict:
4275
  """
4276
  Get assignment details
4277
 
 
4280
 
4281
  Args:
4282
  tool_input: Dict with assignment_id, order_id, or driver_id
4283
+ user_id: Authenticated user ID
4284
 
4285
  Returns:
4286
  Assignment details or list of assignments
4287
  """
4288
+ # Authentication check - allow dev mode
4289
+ if not user_id:
4290
+ if os.getenv("SKIP_AUTH") == "true":
4291
+ user_id = "dev-user"
4292
+ else:
4293
+ return {
4294
+ "success": False,
4295
+ "error": "Authentication required. Please login first.",
4296
+ "auth_required": True
4297
+ }
4298
+
4299
  assignment_id = (tool_input.get("assignment_id") or "").strip()
4300
  order_id = (tool_input.get("order_id") or "").strip()
4301
  driver_id = (tool_input.get("driver_id") or "").strip()
 
4310
  conn = get_db_connection()
4311
  cursor = conn.cursor()
4312
 
4313
+ # Build query based on provided parameters (filtered by user_id)
4314
  query = """
4315
  SELECT
4316
  a.assignment_id, a.order_id, a.driver_id, a.status,
 
4325
  FROM assignments a
4326
  LEFT JOIN orders o ON a.order_id = o.order_id
4327
  LEFT JOIN drivers d ON a.driver_id = d.driver_id
4328
+ WHERE a.user_id = %s
4329
  """
4330
 
4331
+ params = [user_id]
4332
 
4333
  if assignment_id:
4334
  query += " AND a.assignment_id = %s"
 
4428
  }
4429
 
4430
 
4431
+ def handle_update_assignment(tool_input: dict, user_id: str = None) -> dict:
4432
  """
4433
  Update assignment status
4434
 
 
4437
 
4438
  Args:
4439
  tool_input: Dict with assignment_id, status (optional), actual_arrival (optional), notes (optional)
4440
+ user_id: Authenticated user ID
4441
 
4442
  Returns:
4443
  Update result
4444
  """
4445
+ # Authentication check - allow dev mode
4446
+ if not user_id:
4447
+ if os.getenv("SKIP_AUTH") == "true":
4448
+ user_id = "dev-user"
4449
+ else:
4450
+ return {
4451
+ "success": False,
4452
+ "error": "Authentication required. Please login first.",
4453
+ "auth_required": True
4454
+ }
4455
+
4456
  from datetime import datetime
4457
 
4458
  assignment_id = (tool_input.get("assignment_id") or "").strip()
 
4486
  conn = get_db_connection()
4487
  cursor = conn.cursor()
4488
 
4489
+ # Get current assignment details (filtered by user_id)
4490
  cursor.execute("""
4491
  SELECT status, order_id, driver_id
4492
  FROM assignments
4493
+ WHERE assignment_id = %s AND user_id = %s
4494
+ """, (assignment_id, user_id))
4495
 
4496
  assignment_row = cursor.fetchone()
4497
  if not assignment_row:
 
4625
  }
4626
 
4627
 
4628
+ def handle_unassign_order(tool_input: dict, user_id: str = None) -> dict:
4629
  """
4630
  Unassign order (delete assignment)
4631
 
 
4633
 
4634
  Args:
4635
  tool_input: Dict with order_id or assignment_id, and confirm flag
4636
+ user_id: Authenticated user ID
4637
 
4638
  Returns:
4639
  Unassignment result
4640
  """
4641
+ # Authentication check - allow dev mode
4642
+ if not user_id:
4643
+ if os.getenv("SKIP_AUTH") == "true":
4644
+ user_id = "dev-user"
4645
+ else:
4646
+ return {
4647
+ "success": False,
4648
+ "error": "Authentication required. Please login first.",
4649
+ "auth_required": True
4650
+ }
4651
+
4652
  order_id = (tool_input.get("order_id") or "").strip()
4653
  assignment_id = (tool_input.get("assignment_id") or "").strip()
4654
  confirm = tool_input.get("confirm", False)
 
4671
  conn = get_db_connection()
4672
  cursor = conn.cursor()
4673
 
4674
+ # Find assignment (filtered by user_id)
4675
  if assignment_id:
4676
  cursor.execute("""
4677
  SELECT order_id, driver_id, status
4678
  FROM assignments
4679
+ WHERE assignment_id = %s AND user_id = %s
4680
+ """, (assignment_id, user_id))
4681
  else:
4682
  cursor.execute("""
4683
  SELECT assignment_id, driver_id, status
4684
  FROM assignments
4685
+ WHERE order_id = %s AND user_id = %s AND status IN ('active', 'in_progress')
4686
  ORDER BY assigned_at DESC
4687
  LIMIT 1
4688
+ """, (order_id, user_id))
4689
 
4690
  assignment_row = cursor.fetchone()
4691
  if not assignment_row:
 
4767
  }
4768
 
4769
 
4770
+ def handle_complete_delivery(tool_input: dict, user_id: str = None) -> dict:
4771
  """
4772
  Complete a delivery and automatically update driver location
4773
 
 
4776
 
4777
  Args:
4778
  tool_input: Dict with assignment_id, confirm flag, and optional fields
4779
+ user_id: Authenticated user ID
4780
 
4781
  Returns:
4782
  Completion result
4783
  """
4784
+ # Authentication check - allow dev mode
4785
+ if not user_id:
4786
+ if os.getenv("SKIP_AUTH") == "true":
4787
+ user_id = "dev-user"
4788
+ else:
4789
+ return {
4790
+ "success": False,
4791
+ "error": "Authentication required. Please login first.",
4792
+ "auth_required": True
4793
+ }
4794
+
4795
  from datetime import datetime
4796
 
4797
  assignment_id = (tool_input.get("assignment_id") or "").strip()
 
4817
  conn = get_db_connection()
4818
  cursor = conn.cursor()
4819
 
4820
+ # Get assignment and order details including timing fields (filtered by user_id)
4821
  cursor.execute("""
4822
  SELECT
4823
  a.status, a.order_id, a.driver_id,
 
4827
  FROM assignments a
4828
  JOIN orders o ON a.order_id = o.order_id
4829
  JOIN drivers d ON a.driver_id = d.driver_id
4830
+ WHERE a.assignment_id = %s AND a.user_id = %s
4831
+ """, (assignment_id, user_id))
4832
 
4833
  assignment_row = cursor.fetchone()
4834
  if not assignment_row:
 
5001
  }
5002
 
5003
 
5004
+ def handle_fail_delivery(tool_input: dict, user_id: str = None) -> dict:
5005
  """
5006
  Mark delivery as failed with mandatory location and reason
5007
 
 
5011
  Args:
5012
  tool_input: Dict with assignment_id, current_lat, current_lng, failure_reason,
5013
  confirm flag, and optional notes
5014
+ user_id: Authenticated user ID
5015
 
5016
  Returns:
5017
  Failure recording result
5018
  """
5019
+ # Authentication check - allow dev mode
5020
+ if not user_id:
5021
+ if os.getenv("SKIP_AUTH") == "true":
5022
+ user_id = "dev-user"
5023
+ else:
5024
+ return {
5025
+ "success": False,
5026
+ "error": "Authentication required. Please login first.",
5027
+ "auth_required": True
5028
+ }
5029
+
5030
  from datetime import datetime
5031
 
5032
  assignment_id = (tool_input.get("assignment_id") or "").strip()
 
5101
  conn = get_db_connection()
5102
  cursor = conn.cursor()
5103
 
5104
+ # Get assignment and order details including timing fields (filtered by user_id)
5105
  cursor.execute("""
5106
  SELECT
5107
  a.status, a.order_id, a.driver_id,
 
5111
  FROM assignments a
5112
  JOIN orders o ON a.order_id = o.order_id
5113
  JOIN drivers d ON a.driver_id = d.driver_id
5114
+ WHERE a.assignment_id = %s AND a.user_id = %s
5115
+ """, (assignment_id, user_id))
5116
 
5117
  assignment_row = cursor.fetchone()
5118
  if not assignment_row:
database/migrations/007_add_user_id.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Migration 007: Add user_id columns for multi-tenant support
3
+
4
+ This migration adds user_id to all tables to enable user-specific data isolation.
5
+ Each user will only see their own orders, drivers, assignments, etc.
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ # Add parent directory to path
12
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
13
+
14
+ from database.connection import execute_write
15
+
16
+
17
+ def up():
18
+ """Add user_id columns and indexes"""
19
+
20
+ migrations = [
21
+ # Add user_id column to orders table
22
+ """
23
+ ALTER TABLE orders
24
+ ADD COLUMN IF NOT EXISTS user_id VARCHAR(255);
25
+ """,
26
+
27
+ # Add user_id column to drivers table
28
+ """
29
+ ALTER TABLE drivers
30
+ ADD COLUMN IF NOT EXISTS user_id VARCHAR(255);
31
+ """,
32
+
33
+ # Add user_id column to assignments table
34
+ """
35
+ ALTER TABLE assignments
36
+ ADD COLUMN IF NOT EXISTS user_id VARCHAR(255);
37
+ """,
38
+
39
+ # Add user_id column to exceptions table
40
+ """
41
+ ALTER TABLE exceptions
42
+ ADD COLUMN IF NOT EXISTS user_id VARCHAR(255);
43
+ """,
44
+
45
+ # Add user_id column to agent_decisions table
46
+ """
47
+ ALTER TABLE agent_decisions
48
+ ADD COLUMN IF NOT EXISTS user_id VARCHAR(255);
49
+ """,
50
+
51
+ # Add user_id column to metrics table
52
+ """
53
+ ALTER TABLE metrics
54
+ ADD COLUMN IF NOT EXISTS user_id VARCHAR(255);
55
+ """,
56
+
57
+ # Create indexes for fast user-based filtering
58
+ """
59
+ CREATE INDEX IF NOT EXISTS idx_orders_user_id ON orders(user_id);
60
+ """,
61
+
62
+ """
63
+ CREATE INDEX IF NOT EXISTS idx_drivers_user_id ON drivers(user_id);
64
+ """,
65
+
66
+ """
67
+ CREATE INDEX IF NOT EXISTS idx_assignments_user_id ON assignments(user_id);
68
+ """,
69
+
70
+ """
71
+ CREATE INDEX IF NOT EXISTS idx_exceptions_user_id ON exceptions(user_id);
72
+ """,
73
+
74
+ """
75
+ CREATE INDEX IF NOT EXISTS idx_agent_decisions_user_id ON agent_decisions(user_id);
76
+ """,
77
+
78
+ """
79
+ CREATE INDEX IF NOT EXISTS idx_metrics_user_id ON metrics(user_id);
80
+ """,
81
+
82
+ # Create composite indexes for common queries
83
+ """
84
+ CREATE INDEX IF NOT EXISTS idx_orders_user_status ON orders(user_id, status);
85
+ """,
86
+
87
+ """
88
+ CREATE INDEX IF NOT EXISTS idx_orders_user_created ON orders(user_id, created_at DESC);
89
+ """,
90
+
91
+ """
92
+ CREATE INDEX IF NOT EXISTS idx_drivers_user_status ON drivers(user_id, status);
93
+ """,
94
+
95
+ """
96
+ CREATE INDEX IF NOT EXISTS idx_assignments_user_driver ON assignments(user_id, driver_id);
97
+ """,
98
+
99
+ """
100
+ CREATE INDEX IF NOT EXISTS idx_assignments_user_order ON assignments(user_id, order_id);
101
+ """,
102
+ ]
103
+
104
+ print("Migration 007: Adding user_id columns...")
105
+
106
+ for i, sql in enumerate(migrations, 1):
107
+ try:
108
+ print(f" [{i}/{len(migrations)}] Executing: {sql.strip()[:60]}...")
109
+ execute_write(sql)
110
+ print(f" Success")
111
+ except Exception as e:
112
+ print(f" Warning: {e}")
113
+ # Continue even if column already exists
114
+
115
+ print("\nMigration 007 complete!")
116
+ print("\nNext steps:")
117
+ print(" 1. Existing data will have NULL user_id (that's OK for now)")
118
+ print(" 2. New data will automatically get user_id from authentication")
119
+ print(" 3. You can optionally run a data migration to assign existing records to a test user")
120
+
121
+
122
+ def down():
123
+ """Remove user_id columns and indexes (rollback)"""
124
+
125
+ rollback_migrations = [
126
+ # Drop indexes first
127
+ "DROP INDEX IF EXISTS idx_assignments_user_order;",
128
+ "DROP INDEX IF EXISTS idx_assignments_user_driver;",
129
+ "DROP INDEX IF EXISTS idx_drivers_user_status;",
130
+ "DROP INDEX IF EXISTS idx_orders_user_created;",
131
+ "DROP INDEX IF EXISTS idx_orders_user_status;",
132
+ "DROP INDEX IF EXISTS idx_metrics_user_id;",
133
+ "DROP INDEX IF EXISTS idx_agent_decisions_user_id;",
134
+ "DROP INDEX IF EXISTS idx_exceptions_user_id;",
135
+ "DROP INDEX IF EXISTS idx_assignments_user_id;",
136
+ "DROP INDEX IF EXISTS idx_drivers_user_id;",
137
+ "DROP INDEX IF EXISTS idx_orders_user_id;",
138
+
139
+ # Drop columns
140
+ "ALTER TABLE metrics DROP COLUMN IF EXISTS user_id;",
141
+ "ALTER TABLE agent_decisions DROP COLUMN IF EXISTS user_id;",
142
+ "ALTER TABLE exceptions DROP COLUMN IF EXISTS user_id;",
143
+ "ALTER TABLE assignments DROP COLUMN IF EXISTS user_id;",
144
+ "ALTER TABLE drivers DROP COLUMN IF EXISTS user_id;",
145
+ "ALTER TABLE orders DROP COLUMN IF EXISTS user_id;",
146
+ ]
147
+
148
+ print("Rolling back Migration 007...")
149
+
150
+ for i, sql in enumerate(rollback_migrations, 1):
151
+ try:
152
+ print(f" [{i}/{len(rollback_migrations)}] {sql[:60]}...")
153
+ execute_write(sql)
154
+ print(f" Success")
155
+ except Exception as e:
156
+ print(f" Warning: {e}")
157
+
158
+ print("\nRollback complete!")
159
+
160
+
161
+ if __name__ == "__main__":
162
+ import sys
163
+
164
+ if len(sys.argv) > 1 and sys.argv[1] == "down":
165
+ down()
166
+ else:
167
+ up()
database/user_context.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ User Context and Authentication Module
3
+ Handles Stytch authentication and user permission checks
4
+ """
5
+
6
+ from typing import Optional, Dict, Any
7
+ import os
8
+ from stytch import Client
9
+
10
+ # Initialize Stytch client
11
+ stytch_client = None
12
+ if os.getenv('STYTCH_PROJECT_ID') and os.getenv('STYTCH_SECRET'):
13
+ try:
14
+ stytch_client = Client(
15
+ project_id=os.getenv('STYTCH_PROJECT_ID'),
16
+ secret=os.getenv('STYTCH_SECRET')
17
+ )
18
+ print(f"Stytch client initialized with project: {os.getenv('STYTCH_PROJECT_ID')[:20]}...")
19
+ except Exception as e:
20
+ print(f"Warning: Stytch client initialization failed: {e}")
21
+
22
+
23
+ def verify_token(token: str) -> Optional[Dict[str, Any]]:
24
+ """
25
+ Verify authentication token and return user info
26
+
27
+ Args:
28
+ token: Bearer token from Authorization header
29
+
30
+ Returns:
31
+ User info dict with user_id, email, scopes
32
+ None if token is invalid
33
+ """
34
+ # Development mode: Skip authentication
35
+ if os.getenv('ENVIRONMENT') == 'development' and os.getenv('SKIP_AUTH') == 'true':
36
+ return {
37
+ 'user_id': 'dev-user',
38
+ 'email': 'dev@fleetmind.local',
39
+ 'scopes': ['admin'],
40
+ 'name': 'Development User'
41
+ }
42
+
43
+ if not stytch_client:
44
+ print("Error: Stytch client not initialized")
45
+ return None
46
+
47
+ if not token:
48
+ return None
49
+
50
+ try:
51
+ # Verify session token with Stytch
52
+ result = stytch_client.sessions.authenticate(session_token=token)
53
+
54
+ # Extract user information
55
+ user_info = {
56
+ 'user_id': result.user.user_id,
57
+ 'email': result.user.emails[0].email if result.user.emails else 'unknown',
58
+ 'scopes': result.session.custom_claims.get('scopes', ['orders:read', 'drivers:read']),
59
+ 'name': result.user.name if hasattr(result.user, 'name') else 'Unknown User'
60
+ }
61
+
62
+ return user_info
63
+
64
+ except Exception as e:
65
+ print(f"Token validation failed: {e}")
66
+ return None
67
+
68
+
69
+ def check_permission(user_scopes: list, required_scope: str) -> bool:
70
+ """
71
+ Check if user has required permission
72
+
73
+ Args:
74
+ user_scopes: List of scopes user has
75
+ required_scope: Scope needed for this operation
76
+
77
+ Returns:
78
+ True if user has permission
79
+ """
80
+ # Admin has all permissions
81
+ if 'admin' in user_scopes:
82
+ return True
83
+
84
+ # Check specific scope
85
+ return required_scope in user_scopes
86
+
87
+
88
+ # Scope requirements for each tool
89
+ SCOPE_REQUIREMENTS = {
90
+ # Order operations
91
+ 'create_order': 'orders:write',
92
+ 'fetch_orders': 'orders:read',
93
+ 'update_order': 'orders:write',
94
+ 'delete_order': 'orders:write',
95
+ 'search_orders': 'orders:read',
96
+ 'get_order_details': 'orders:read',
97
+ 'count_orders': 'orders:read',
98
+ 'get_incomplete_orders': 'orders:read',
99
+
100
+ # Driver operations
101
+ 'create_driver': 'drivers:write',
102
+ 'fetch_drivers': 'drivers:read',
103
+ 'update_driver': 'drivers:write',
104
+ 'delete_driver': 'drivers:write',
105
+ 'search_drivers': 'drivers:read',
106
+ 'get_driver_details': 'drivers:read',
107
+ 'count_drivers': 'drivers:read',
108
+ 'get_available_drivers': 'drivers:read',
109
+
110
+ # Assignment operations
111
+ 'create_assignment': 'assignments:manage',
112
+ 'auto_assign_order': 'assignments:manage',
113
+ 'intelligent_assign_order': 'assignments:manage',
114
+ 'get_assignment_details': 'assignments:manage',
115
+ 'update_assignment': 'assignments:manage',
116
+ 'unassign_order': 'assignments:manage',
117
+ 'complete_delivery': 'assignments:manage',
118
+ 'fail_delivery': 'assignments:manage',
119
+
120
+ # Routing (public - no scope required)
121
+ 'geocode_address': None,
122
+ 'calculate_route': None,
123
+ 'calculate_intelligent_route': None,
124
+
125
+ # Dangerous operations (admin only)
126
+ 'delete_all_orders': 'admin',
127
+ 'delete_all_drivers': 'admin',
128
+ }
129
+
130
+
131
+ def get_required_scope(tool_name: str) -> Optional[str]:
132
+ """
133
+ Get the scope required for a tool
134
+
135
+ Args:
136
+ tool_name: Name of the tool
137
+
138
+ Returns:
139
+ Required scope or None if tool is public
140
+ """
141
+ return SCOPE_REQUIREMENTS.get(tool_name, 'admin')
server.py CHANGED
@@ -18,11 +18,16 @@ from datetime import datetime
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
  # Configure logging
27
  logging.basicConfig(
28
  level=logging.INFO,
@@ -34,10 +39,65 @@ logging.basicConfig(
34
  )
35
  logger = logging.getLogger(__name__)
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  # ============================================================================
38
  # MCP SERVER INITIALIZATION
39
  # ============================================================================
40
 
 
 
41
  mcp = FastMCP(
42
  name="FleetMind Dispatch Coordinator",
43
  version="1.0.0"
@@ -55,6 +115,60 @@ try:
55
  except Exception as e:
56
  logger.error(f"Database: Connection failed - {e}")
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  # ============================================================================
59
  # MCP RESOURCES
60
  # ============================================================================
@@ -345,22 +459,44 @@ def create_order(
345
  message: str
346
  }
347
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  from chat.tools import handle_create_order
349
- logger.info(f"Tool: create_order(customer='{customer_name}', expected_delivery='{expected_delivery_time}')")
350
- return handle_create_order({
351
- "customer_name": customer_name,
352
- "delivery_address": delivery_address,
353
- "delivery_lat": delivery_lat,
354
- "delivery_lng": delivery_lng,
355
- "expected_delivery_time": expected_delivery_time,
356
- "customer_phone": customer_phone,
357
- "customer_email": customer_email,
358
- "priority": priority,
359
- "weight_kg": weight_kg,
360
- "special_instructions": special_instructions,
361
- "sla_grace_period_minutes": sla_grace_period_minutes,
362
- "time_window_end": time_window_end
363
- })
 
 
 
 
364
 
365
 
366
  # ============================================================================
@@ -401,6 +537,18 @@ def count_orders(
401
  """
402
  from chat.tools import handle_count_orders
403
  logger.info(f"Tool: count_orders(status={status}, priority={priority})")
 
 
 
 
 
 
 
 
 
 
 
 
404
  tool_input = {}
405
  if status is not None:
406
  tool_input["status"] = status
@@ -416,7 +564,7 @@ def count_orders(
416
  tool_input["requires_signature"] = requires_signature
417
  if requires_cold_storage is not None:
418
  tool_input["requires_cold_storage"] = requires_cold_storage
419
- return handle_count_orders(tool_input)
420
 
421
 
422
  @mcp.tool()
@@ -460,6 +608,18 @@ def fetch_orders(
460
  """
461
  from chat.tools import handle_fetch_orders
462
  logger.info(f"Tool: fetch_orders(limit={limit}, offset={offset}, status={status})")
 
 
 
 
 
 
 
 
 
 
 
 
463
  tool_input = {
464
  "limit": limit,
465
  "offset": offset,
@@ -480,7 +640,7 @@ def fetch_orders(
480
  tool_input["requires_signature"] = requires_signature
481
  if requires_cold_storage is not None:
482
  tool_input["requires_cold_storage"] = requires_cold_storage
483
- return handle_fetch_orders(tool_input)
484
 
485
 
486
  @mcp.tool()
@@ -501,7 +661,19 @@ def get_order_details(order_id: str) -> dict:
501
  """
502
  from chat.tools import handle_get_order_details
503
  logger.info(f"Tool: get_order_details(order_id='{order_id}')")
504
- return handle_get_order_details({"order_id": order_id})
 
 
 
 
 
 
 
 
 
 
 
 
505
 
506
 
507
  @mcp.tool()
@@ -523,7 +695,19 @@ def search_orders(search_term: str) -> dict:
523
  """
524
  from chat.tools import handle_search_orders
525
  logger.info(f"Tool: search_orders(search_term='{search_term}')")
526
- return handle_search_orders({"search_term": search_term})
 
 
 
 
 
 
 
 
 
 
 
 
527
 
528
 
529
  @mcp.tool()
@@ -545,7 +729,19 @@ def get_incomplete_orders(limit: int = 20) -> dict:
545
  """
546
  from chat.tools import handle_get_incomplete_orders
547
  logger.info(f"Tool: get_incomplete_orders(limit={limit})")
548
- return handle_get_incomplete_orders({"limit": limit})
 
 
 
 
 
 
 
 
 
 
 
 
549
 
550
 
551
  # ============================================================================
@@ -599,6 +795,18 @@ def update_order(
599
  """
600
  from chat.tools import handle_update_order
601
  logger.info(f"Tool: update_order(order_id='{order_id}')")
 
 
 
 
 
 
 
 
 
 
 
 
602
  tool_input = {"order_id": order_id}
603
  if customer_name is not None:
604
  tool_input["customer_name"] = customer_name
@@ -626,7 +834,7 @@ def update_order(
626
  tool_input["weight_kg"] = weight_kg
627
  if order_value is not None:
628
  tool_input["order_value"] = order_value
629
- return handle_update_order(tool_input)
630
 
631
 
632
  @mcp.tool()
@@ -647,7 +855,19 @@ def delete_order(order_id: str, confirm: bool) -> dict:
647
  """
648
  from chat.tools import handle_delete_order
649
  logger.info(f"Tool: delete_order(order_id='{order_id}', confirm={confirm})")
650
- return handle_delete_order({"order_id": order_id, "confirm": confirm})
 
 
 
 
 
 
 
 
 
 
 
 
651
 
652
 
653
  # ============================================================================
@@ -695,6 +915,18 @@ def create_driver(
695
  """
696
  from chat.tools import handle_create_driver
697
  logger.info(f"Tool: create_driver(name='{name}', vehicle_type='{vehicle_type}')")
 
 
 
 
 
 
 
 
 
 
 
 
698
  return handle_create_driver({
699
  "name": name,
700
  "phone": phone,
@@ -705,7 +937,7 @@ def create_driver(
705
  "capacity_m3": capacity_m3,
706
  "skills": skills or [],
707
  "status": status
708
- })
709
 
710
 
711
  # ============================================================================
@@ -736,12 +968,24 @@ def count_drivers(
736
  """
737
  from chat.tools import handle_count_drivers
738
  logger.info(f"Tool: count_drivers(status={status}, vehicle_type={vehicle_type})")
 
 
 
 
 
 
 
 
 
 
 
 
739
  tool_input = {}
740
  if status is not None:
741
  tool_input["status"] = status
742
  if vehicle_type is not None:
743
  tool_input["vehicle_type"] = vehicle_type
744
- return handle_count_drivers(tool_input)
745
 
746
 
747
  @mcp.tool()
@@ -775,6 +1019,18 @@ def fetch_drivers(
775
  """
776
  from chat.tools import handle_fetch_drivers
777
  logger.info(f"Tool: fetch_drivers(limit={limit}, offset={offset}, status={status})")
 
 
 
 
 
 
 
 
 
 
 
 
778
  tool_input = {
779
  "limit": limit,
780
  "offset": offset,
@@ -785,7 +1041,7 @@ def fetch_drivers(
785
  tool_input["status"] = status
786
  if vehicle_type is not None:
787
  tool_input["vehicle_type"] = vehicle_type
788
- return handle_fetch_drivers(tool_input)
789
 
790
 
791
  @mcp.tool()
@@ -808,7 +1064,19 @@ def get_driver_details(driver_id: str) -> dict:
808
  """
809
  from chat.tools import handle_get_driver_details
810
  logger.info(f"Tool: get_driver_details(driver_id='{driver_id}')")
811
- return handle_get_driver_details({"driver_id": driver_id})
 
 
 
 
 
 
 
 
 
 
 
 
812
 
813
 
814
  @mcp.tool()
@@ -830,7 +1098,19 @@ def search_drivers(search_term: str) -> dict:
830
  """
831
  from chat.tools import handle_search_drivers
832
  logger.info(f"Tool: search_drivers(search_term='{search_term}')")
833
- return handle_search_drivers({"search_term": search_term})
 
 
 
 
 
 
 
 
 
 
 
 
834
 
835
 
836
  @mcp.tool()
@@ -852,7 +1132,19 @@ def get_available_drivers(limit: int = 20) -> dict:
852
  """
853
  from chat.tools import handle_get_available_drivers
854
  logger.info(f"Tool: get_available_drivers(limit={limit})")
855
- return handle_get_available_drivers({"limit": limit})
 
 
 
 
 
 
 
 
 
 
 
 
856
 
857
 
858
  # ============================================================================
@@ -902,6 +1194,18 @@ def update_driver(
902
  """
903
  from chat.tools import handle_update_driver
904
  logger.info(f"Tool: update_driver(driver_id='{driver_id}')")
 
 
 
 
 
 
 
 
 
 
 
 
905
  tool_input = {"driver_id": driver_id}
906
  if name is not None:
907
  tool_input["name"] = name
@@ -925,7 +1229,7 @@ def update_driver(
925
  tool_input["current_lat"] = current_lat
926
  if current_lng is not None:
927
  tool_input["current_lng"] = current_lng
928
- return handle_update_driver(tool_input)
929
 
930
 
931
  @mcp.tool()
@@ -946,7 +1250,19 @@ def delete_driver(driver_id: str, confirm: bool) -> dict:
946
  """
947
  from chat.tools import handle_delete_driver
948
  logger.info(f"Tool: delete_driver(driver_id='{driver_id}', confirm={confirm})")
949
- return handle_delete_driver({"driver_id": driver_id, "confirm": confirm})
 
 
 
 
 
 
 
 
 
 
 
 
950
 
951
 
952
  @mcp.tool()
@@ -972,7 +1288,19 @@ def delete_all_orders(confirm: bool, status: str = None) -> dict:
972
  """
973
  from chat.tools import handle_delete_all_orders
974
  logger.info(f"Tool: delete_all_orders(confirm={confirm}, status='{status}')")
975
- return handle_delete_all_orders({"confirm": confirm, "status": status})
 
 
 
 
 
 
 
 
 
 
 
 
976
 
977
 
978
  @mcp.tool()
@@ -998,7 +1326,19 @@ def delete_all_drivers(confirm: bool, status: str = None) -> dict:
998
  """
999
  from chat.tools import handle_delete_all_drivers
1000
  logger.info(f"Tool: delete_all_drivers(confirm={confirm}, status='{status}')")
1001
- return handle_delete_all_drivers({"confirm": confirm, "status": status})
 
 
 
 
 
 
 
 
 
 
 
 
1002
 
1003
 
1004
  # ============================================================================
@@ -1042,7 +1382,19 @@ def create_assignment(order_id: str, driver_id: str) -> dict:
1042
  """
1043
  from chat.tools import handle_create_assignment
1044
  logger.info(f"Tool: create_assignment(order_id='{order_id}', driver_id='{driver_id}')")
1045
- return handle_create_assignment({"order_id": order_id, "driver_id": driver_id})
 
 
 
 
 
 
 
 
 
 
 
 
1046
 
1047
 
1048
  @mcp.tool()
@@ -1090,7 +1442,19 @@ def auto_assign_order(order_id: str) -> dict:
1090
  """
1091
  from chat.tools import handle_auto_assign_order
1092
  logger.info(f"Tool: auto_assign_order(order_id='{order_id}')")
1093
- return handle_auto_assign_order({"order_id": order_id})
 
 
 
 
 
 
 
 
 
 
 
 
1094
 
1095
 
1096
  @mcp.tool()
@@ -1158,7 +1522,19 @@ def intelligent_assign_order(order_id: str) -> dict:
1158
  """
1159
  from chat.tools import handle_intelligent_assign_order
1160
  logger.info(f"Tool: intelligent_assign_order(order_id='{order_id}')")
1161
- return handle_intelligent_assign_order({"order_id": order_id})
 
 
 
 
 
 
 
 
 
 
 
 
1162
 
1163
 
1164
  @mcp.tool()
@@ -1201,11 +1577,23 @@ def get_assignment_details(
1201
  """
1202
  from chat.tools import handle_get_assignment_details
1203
  logger.info(f"Tool: get_assignment_details(assignment_id='{assignment_id}', order_id='{order_id}', driver_id='{driver_id}')")
 
 
 
 
 
 
 
 
 
 
 
 
1204
  return handle_get_assignment_details({
1205
  "assignment_id": assignment_id,
1206
  "order_id": order_id,
1207
  "driver_id": driver_id
1208
- })
1209
 
1210
 
1211
  @mcp.tool()
@@ -1248,13 +1636,25 @@ def update_assignment(
1248
  """
1249
  from chat.tools import handle_update_assignment
1250
  logger.info(f"Tool: update_assignment(assignment_id='{assignment_id}', status='{status}')")
 
 
 
 
 
 
 
 
 
 
 
 
1251
  return handle_update_assignment({
1252
  "assignment_id": assignment_id,
1253
  "status": status,
1254
  "actual_arrival": actual_arrival,
1255
  "actual_distance_meters": actual_distance_meters,
1256
  "notes": notes
1257
- })
1258
 
1259
 
1260
  @mcp.tool()
@@ -1287,7 +1687,19 @@ def unassign_order(assignment_id: str, confirm: bool = False) -> dict:
1287
  """
1288
  from chat.tools import handle_unassign_order
1289
  logger.info(f"Tool: unassign_order(assignment_id='{assignment_id}', confirm={confirm})")
1290
- return handle_unassign_order({"assignment_id": assignment_id, "confirm": confirm})
 
 
 
 
 
 
 
 
 
 
 
 
1291
 
1292
 
1293
  @mcp.tool()
@@ -1337,12 +1749,24 @@ def complete_delivery(
1337
  """
1338
  from chat.tools import handle_complete_delivery
1339
  logger.info(f"Tool: complete_delivery(assignment_id='{assignment_id}', confirm={confirm})")
 
 
 
 
 
 
 
 
 
 
 
 
1340
  return handle_complete_delivery({
1341
  "assignment_id": assignment_id,
1342
  "confirm": confirm,
1343
  "actual_distance_meters": actual_distance_meters,
1344
  "notes": notes
1345
- })
1346
 
1347
 
1348
  @mcp.tool()
@@ -1411,6 +1835,18 @@ def fail_delivery(
1411
  """
1412
  from chat.tools import handle_fail_delivery
1413
  logger.info(f"Tool: fail_delivery(assignment_id='{assignment_id}', reason='{failure_reason}')")
 
 
 
 
 
 
 
 
 
 
 
 
1414
  return handle_fail_delivery({
1415
  "assignment_id": assignment_id,
1416
  "current_lat": current_lat,
@@ -1418,7 +1854,7 @@ def fail_delivery(
1418
  "failure_reason": failure_reason,
1419
  "confirm": confirm,
1420
  "notes": notes
1421
- })
1422
 
1423
 
1424
  # ============================================================================
 
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 authentication module
29
+ from database.user_context import verify_token, check_permission, get_required_scope
30
+
31
  # Configure logging
32
  logging.basicConfig(
33
  level=logging.INFO,
 
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"
 
115
  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 from authentication token via FastMCP
128
+
129
+ Returns:
130
+ User info dict with user_id, email, scopes, name or None if not authenticated
131
+ """
132
+ try:
133
+ # Development bypass mode
134
+ if os.getenv("SKIP_AUTH") == "true":
135
+ logger.debug("SKIP_AUTH enabled - using development user")
136
+ return {
137
+ 'user_id': 'dev-user',
138
+ 'email': 'dev@fleetmind.local',
139
+ 'scopes': ['admin'],
140
+ 'name': 'Development User'
141
+ }
142
+
143
+ # Extract token from FastMCP request context
144
+ from fastmcp.server.dependencies import get_access_token
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
171
+
172
  # ============================================================================
173
  # MCP RESOURCES
174
  # ============================================================================
 
459
  message: str
460
  }
461
  """
462
+ # STEP 1: Authenticate user
463
+ user = get_authenticated_user()
464
+ if not user:
465
+ return {
466
+ "success": False,
467
+ "error": "Authentication required. Please login first.",
468
+ "auth_required": True
469
+ }
470
+
471
+ # STEP 2: Check permissions
472
+ required_scope = get_required_scope('create_order')
473
+ if not check_permission(user.get('scopes', []), required_scope):
474
+ return {
475
+ "success": False,
476
+ "error": f"Permission denied. Required scope: {required_scope}"
477
+ }
478
+
479
+ # STEP 3: Execute tool with user_id
480
  from chat.tools import handle_create_order
481
+ logger.info(f"Tool: create_order by user {user.get('email')} (customer='{customer_name}')")
482
+
483
+ return handle_create_order(
484
+ tool_input={
485
+ "customer_name": customer_name,
486
+ "delivery_address": delivery_address,
487
+ "delivery_lat": delivery_lat,
488
+ "delivery_lng": delivery_lng,
489
+ "expected_delivery_time": expected_delivery_time,
490
+ "customer_phone": customer_phone,
491
+ "customer_email": customer_email,
492
+ "priority": priority,
493
+ "weight_kg": weight_kg,
494
+ "special_instructions": special_instructions,
495
+ "sla_grace_period_minutes": sla_grace_period_minutes,
496
+ "time_window_end": time_window_end
497
+ },
498
+ user_id=user['user_id'] # Pass user_id for data isolation
499
+ )
500
 
501
 
502
  # ============================================================================
 
537
  """
538
  from chat.tools import handle_count_orders
539
  logger.info(f"Tool: count_orders(status={status}, priority={priority})")
540
+
541
+ # STEP 1: Authenticate user
542
+ user = get_authenticated_user()
543
+ if not user:
544
+ return {"success": False, "error": "Authentication required"}
545
+
546
+ # STEP 2: Check permissions
547
+ required_scope = get_required_scope('count_orders')
548
+ if not check_permission(user.get('scopes', []), required_scope):
549
+ return {"success": False, "error": "Permission denied"}
550
+
551
+ # STEP 3: Execute with user_id
552
  tool_input = {}
553
  if status is not None:
554
  tool_input["status"] = status
 
564
  tool_input["requires_signature"] = requires_signature
565
  if requires_cold_storage is not None:
566
  tool_input["requires_cold_storage"] = requires_cold_storage
567
+ return handle_count_orders(tool_input, user_id=user['user_id'])
568
 
569
 
570
  @mcp.tool()
 
608
  """
609
  from chat.tools import handle_fetch_orders
610
  logger.info(f"Tool: fetch_orders(limit={limit}, offset={offset}, status={status})")
611
+
612
+ # STEP 1: Authenticate user
613
+ user = get_authenticated_user()
614
+ if not user:
615
+ return {"success": False, "error": "Authentication required"}
616
+
617
+ # STEP 2: Check permissions
618
+ required_scope = get_required_scope('fetch_orders')
619
+ if not check_permission(user.get('scopes', []), required_scope):
620
+ return {"success": False, "error": "Permission denied"}
621
+
622
+ # STEP 3: Execute with user_id
623
  tool_input = {
624
  "limit": limit,
625
  "offset": offset,
 
640
  tool_input["requires_signature"] = requires_signature
641
  if requires_cold_storage is not None:
642
  tool_input["requires_cold_storage"] = requires_cold_storage
643
+ return handle_fetch_orders(tool_input, user_id=user['user_id'])
644
 
645
 
646
  @mcp.tool()
 
661
  """
662
  from chat.tools import handle_get_order_details
663
  logger.info(f"Tool: get_order_details(order_id='{order_id}')")
664
+
665
+ # STEP 1: Authenticate user
666
+ user = get_authenticated_user()
667
+ if not user:
668
+ return {"success": False, "error": "Authentication required"}
669
+
670
+ # STEP 2: Check permissions
671
+ required_scope = get_required_scope('get_order_details')
672
+ if not check_permission(user.get('scopes', []), required_scope):
673
+ return {"success": False, "error": "Permission denied"}
674
+
675
+ # STEP 3: Execute with user_id
676
+ return handle_get_order_details({"order_id": order_id}, user_id=user['user_id'])
677
 
678
 
679
  @mcp.tool()
 
695
  """
696
  from chat.tools import handle_search_orders
697
  logger.info(f"Tool: search_orders(search_term='{search_term}')")
698
+
699
+ # STEP 1: Authenticate user
700
+ user = get_authenticated_user()
701
+ if not user:
702
+ return {"success": False, "error": "Authentication required"}
703
+
704
+ # STEP 2: Check permissions
705
+ required_scope = get_required_scope('search_orders')
706
+ if not check_permission(user.get('scopes', []), required_scope):
707
+ return {"success": False, "error": "Permission denied"}
708
+
709
+ # STEP 3: Execute with user_id
710
+ return handle_search_orders({"search_term": search_term}, user_id=user['user_id'])
711
 
712
 
713
  @mcp.tool()
 
729
  """
730
  from chat.tools import handle_get_incomplete_orders
731
  logger.info(f"Tool: get_incomplete_orders(limit={limit})")
732
+
733
+ # STEP 1: Authenticate user
734
+ user = get_authenticated_user()
735
+ if not user:
736
+ return {"success": False, "error": "Authentication required"}
737
+
738
+ # STEP 2: Check permissions
739
+ required_scope = get_required_scope('get_incomplete_orders')
740
+ if not check_permission(user.get('scopes', []), required_scope):
741
+ return {"success": False, "error": "Permission denied"}
742
+
743
+ # STEP 3: Execute with user_id
744
+ return handle_get_incomplete_orders({"limit": limit}, user_id=user['user_id'])
745
 
746
 
747
  # ============================================================================
 
795
  """
796
  from chat.tools import handle_update_order
797
  logger.info(f"Tool: update_order(order_id='{order_id}')")
798
+
799
+ # STEP 1: Authenticate user
800
+ user = get_authenticated_user()
801
+ if not user:
802
+ return {"success": False, "error": "Authentication required"}
803
+
804
+ # STEP 2: Check permissions
805
+ required_scope = get_required_scope('update_order')
806
+ if not check_permission(user.get('scopes', []), required_scope):
807
+ return {"success": False, "error": "Permission denied"}
808
+
809
+ # STEP 3: Execute with user_id
810
  tool_input = {"order_id": order_id}
811
  if customer_name is not None:
812
  tool_input["customer_name"] = customer_name
 
834
  tool_input["weight_kg"] = weight_kg
835
  if order_value is not None:
836
  tool_input["order_value"] = order_value
837
+ return handle_update_order(tool_input, user_id=user['user_id'])
838
 
839
 
840
  @mcp.tool()
 
855
  """
856
  from chat.tools import handle_delete_order
857
  logger.info(f"Tool: delete_order(order_id='{order_id}', confirm={confirm})")
858
+
859
+ # STEP 1: Authenticate user
860
+ user = get_authenticated_user()
861
+ if not user:
862
+ return {"success": False, "error": "Authentication required"}
863
+
864
+ # STEP 2: Check permissions
865
+ required_scope = get_required_scope('delete_order')
866
+ if not check_permission(user.get('scopes', []), required_scope):
867
+ return {"success": False, "error": "Permission denied"}
868
+
869
+ # STEP 3: Execute with user_id
870
+ return handle_delete_order({"order_id": order_id, "confirm": confirm}, user_id=user['user_id'])
871
 
872
 
873
  # ============================================================================
 
915
  """
916
  from chat.tools import handle_create_driver
917
  logger.info(f"Tool: create_driver(name='{name}', vehicle_type='{vehicle_type}')")
918
+
919
+ # STEP 1: Authenticate user
920
+ user = get_authenticated_user()
921
+ if not user:
922
+ return {"success": False, "error": "Authentication required"}
923
+
924
+ # STEP 2: Check permissions
925
+ required_scope = get_required_scope('create_driver')
926
+ if not check_permission(user.get('scopes', []), required_scope):
927
+ return {"success": False, "error": "Permission denied"}
928
+
929
+ # STEP 3: Execute with user_id
930
  return handle_create_driver({
931
  "name": name,
932
  "phone": phone,
 
937
  "capacity_m3": capacity_m3,
938
  "skills": skills or [],
939
  "status": status
940
+ }, user_id=user['user_id'])
941
 
942
 
943
  # ============================================================================
 
968
  """
969
  from chat.tools import handle_count_drivers
970
  logger.info(f"Tool: count_drivers(status={status}, vehicle_type={vehicle_type})")
971
+
972
+ # STEP 1: Authenticate user
973
+ user = get_authenticated_user()
974
+ if not user:
975
+ return {"success": False, "error": "Authentication required"}
976
+
977
+ # STEP 2: Check permissions
978
+ required_scope = get_required_scope('count_drivers')
979
+ if not check_permission(user.get('scopes', []), required_scope):
980
+ return {"success": False, "error": "Permission denied"}
981
+
982
+ # STEP 3: Execute with user_id
983
  tool_input = {}
984
  if status is not None:
985
  tool_input["status"] = status
986
  if vehicle_type is not None:
987
  tool_input["vehicle_type"] = vehicle_type
988
+ return handle_count_drivers(tool_input, user_id=user['user_id'])
989
 
990
 
991
  @mcp.tool()
 
1019
  """
1020
  from chat.tools import handle_fetch_drivers
1021
  logger.info(f"Tool: fetch_drivers(limit={limit}, offset={offset}, status={status})")
1022
+
1023
+ # STEP 1: Authenticate user
1024
+ user = get_authenticated_user()
1025
+ if not user:
1026
+ return {"success": False, "error": "Authentication required"}
1027
+
1028
+ # STEP 2: Check permissions
1029
+ required_scope = get_required_scope('fetch_drivers')
1030
+ if not check_permission(user.get('scopes', []), required_scope):
1031
+ return {"success": False, "error": "Permission denied"}
1032
+
1033
+ # STEP 3: Execute with user_id
1034
  tool_input = {
1035
  "limit": limit,
1036
  "offset": offset,
 
1041
  tool_input["status"] = status
1042
  if vehicle_type is not None:
1043
  tool_input["vehicle_type"] = vehicle_type
1044
+ return handle_fetch_drivers(tool_input, user_id=user['user_id'])
1045
 
1046
 
1047
  @mcp.tool()
 
1064
  """
1065
  from chat.tools import handle_get_driver_details
1066
  logger.info(f"Tool: get_driver_details(driver_id='{driver_id}')")
1067
+
1068
+ # STEP 1: Authenticate user
1069
+ user = get_authenticated_user()
1070
+ if not user:
1071
+ return {"success": False, "error": "Authentication required"}
1072
+
1073
+ # STEP 2: Check permissions
1074
+ required_scope = get_required_scope('get_driver_details')
1075
+ if not check_permission(user.get('scopes', []), required_scope):
1076
+ return {"success": False, "error": "Permission denied"}
1077
+
1078
+ # STEP 3: Execute with user_id
1079
+ return handle_get_driver_details({"driver_id": driver_id}, user_id=user['user_id'])
1080
 
1081
 
1082
  @mcp.tool()
 
1098
  """
1099
  from chat.tools import handle_search_drivers
1100
  logger.info(f"Tool: search_drivers(search_term='{search_term}')")
1101
+
1102
+ # STEP 1: Authenticate user
1103
+ user = get_authenticated_user()
1104
+ if not user:
1105
+ return {"success": False, "error": "Authentication required"}
1106
+
1107
+ # STEP 2: Check permissions
1108
+ required_scope = get_required_scope('search_drivers')
1109
+ if not check_permission(user.get('scopes', []), required_scope):
1110
+ return {"success": False, "error": "Permission denied"}
1111
+
1112
+ # STEP 3: Execute with user_id
1113
+ return handle_search_drivers({"search_term": search_term}, user_id=user['user_id'])
1114
 
1115
 
1116
  @mcp.tool()
 
1132
  """
1133
  from chat.tools import handle_get_available_drivers
1134
  logger.info(f"Tool: get_available_drivers(limit={limit})")
1135
+
1136
+ # STEP 1: Authenticate user
1137
+ user = get_authenticated_user()
1138
+ if not user:
1139
+ return {"success": False, "error": "Authentication required"}
1140
+
1141
+ # STEP 2: Check permissions
1142
+ required_scope = get_required_scope('get_available_drivers')
1143
+ if not check_permission(user.get('scopes', []), required_scope):
1144
+ return {"success": False, "error": "Permission denied"}
1145
+
1146
+ # STEP 3: Execute with user_id
1147
+ return handle_get_available_drivers({"limit": limit}, user_id=user['user_id'])
1148
 
1149
 
1150
  # ============================================================================
 
1194
  """
1195
  from chat.tools import handle_update_driver
1196
  logger.info(f"Tool: update_driver(driver_id='{driver_id}')")
1197
+
1198
+ # STEP 1: Authenticate user
1199
+ user = get_authenticated_user()
1200
+ if not user:
1201
+ return {"success": False, "error": "Authentication required"}
1202
+
1203
+ # STEP 2: Check permissions
1204
+ required_scope = get_required_scope('update_driver')
1205
+ if not check_permission(user.get('scopes', []), required_scope):
1206
+ return {"success": False, "error": "Permission denied"}
1207
+
1208
+ # STEP 3: Execute with user_id
1209
  tool_input = {"driver_id": driver_id}
1210
  if name is not None:
1211
  tool_input["name"] = name
 
1229
  tool_input["current_lat"] = current_lat
1230
  if current_lng is not None:
1231
  tool_input["current_lng"] = current_lng
1232
+ return handle_update_driver(tool_input, user_id=user['user_id'])
1233
 
1234
 
1235
  @mcp.tool()
 
1250
  """
1251
  from chat.tools import handle_delete_driver
1252
  logger.info(f"Tool: delete_driver(driver_id='{driver_id}', confirm={confirm})")
1253
+
1254
+ # STEP 1: Authenticate user
1255
+ user = get_authenticated_user()
1256
+ if not user:
1257
+ return {"success": False, "error": "Authentication required"}
1258
+
1259
+ # STEP 2: Check permissions
1260
+ required_scope = get_required_scope('delete_driver')
1261
+ if not check_permission(user.get('scopes', []), required_scope):
1262
+ return {"success": False, "error": "Permission denied"}
1263
+
1264
+ # STEP 3: Execute with user_id
1265
+ return handle_delete_driver({"driver_id": driver_id, "confirm": confirm}, user_id=user['user_id'])
1266
 
1267
 
1268
  @mcp.tool()
 
1288
  """
1289
  from chat.tools import handle_delete_all_orders
1290
  logger.info(f"Tool: delete_all_orders(confirm={confirm}, status='{status}')")
1291
+
1292
+ # STEP 1: Authenticate user
1293
+ user = get_authenticated_user()
1294
+ if not user:
1295
+ return {"success": False, "error": "Authentication required"}
1296
+
1297
+ # STEP 2: Check permissions
1298
+ required_scope = get_required_scope('delete_all_orders')
1299
+ if not check_permission(user.get('scopes', []), required_scope):
1300
+ return {"success": False, "error": "Permission denied"}
1301
+
1302
+ # STEP 3: Execute with user_id
1303
+ return handle_delete_all_orders({"confirm": confirm, "status": status}, user_id=user['user_id'])
1304
 
1305
 
1306
  @mcp.tool()
 
1326
  """
1327
  from chat.tools import handle_delete_all_drivers
1328
  logger.info(f"Tool: delete_all_drivers(confirm={confirm}, status='{status}')")
1329
+
1330
+ # STEP 1: Authenticate user
1331
+ user = get_authenticated_user()
1332
+ if not user:
1333
+ return {"success": False, "error": "Authentication required"}
1334
+
1335
+ # STEP 2: Check permissions
1336
+ required_scope = get_required_scope('delete_all_drivers')
1337
+ if not check_permission(user.get('scopes', []), required_scope):
1338
+ return {"success": False, "error": "Permission denied"}
1339
+
1340
+ # STEP 3: Execute with user_id
1341
+ return handle_delete_all_drivers({"confirm": confirm, "status": status}, user_id=user['user_id'])
1342
 
1343
 
1344
  # ============================================================================
 
1382
  """
1383
  from chat.tools import handle_create_assignment
1384
  logger.info(f"Tool: create_assignment(order_id='{order_id}', driver_id='{driver_id}')")
1385
+
1386
+ # STEP 1: Authenticate user
1387
+ user = get_authenticated_user()
1388
+ if not user:
1389
+ return {"success": False, "error": "Authentication required"}
1390
+
1391
+ # STEP 2: Check permissions
1392
+ required_scope = get_required_scope('create_assignment')
1393
+ if not check_permission(user.get('scopes', []), required_scope):
1394
+ return {"success": False, "error": "Permission denied"}
1395
+
1396
+ # STEP 3: Execute with user_id
1397
+ return handle_create_assignment({"order_id": order_id, "driver_id": driver_id}, user_id=user['user_id'])
1398
 
1399
 
1400
  @mcp.tool()
 
1442
  """
1443
  from chat.tools import handle_auto_assign_order
1444
  logger.info(f"Tool: auto_assign_order(order_id='{order_id}')")
1445
+
1446
+ # STEP 1: Authenticate user
1447
+ user = get_authenticated_user()
1448
+ if not user:
1449
+ return {"success": False, "error": "Authentication required"}
1450
+
1451
+ # STEP 2: Check permissions
1452
+ required_scope = get_required_scope('auto_assign_order')
1453
+ if not check_permission(user.get('scopes', []), required_scope):
1454
+ return {"success": False, "error": "Permission denied"}
1455
+
1456
+ # STEP 3: Execute with user_id
1457
+ return handle_auto_assign_order({"order_id": order_id}, user_id=user['user_id'])
1458
 
1459
 
1460
  @mcp.tool()
 
1522
  """
1523
  from chat.tools import handle_intelligent_assign_order
1524
  logger.info(f"Tool: intelligent_assign_order(order_id='{order_id}')")
1525
+
1526
+ # STEP 1: Authenticate user
1527
+ user = get_authenticated_user()
1528
+ if not user:
1529
+ return {"success": False, "error": "Authentication required"}
1530
+
1531
+ # STEP 2: Check permissions
1532
+ required_scope = get_required_scope('intelligent_assign_order')
1533
+ if not check_permission(user.get('scopes', []), required_scope):
1534
+ return {"success": False, "error": "Permission denied"}
1535
+
1536
+ # STEP 3: Execute with user_id
1537
+ return handle_intelligent_assign_order({"order_id": order_id}, user_id=user['user_id'])
1538
 
1539
 
1540
  @mcp.tool()
 
1577
  """
1578
  from chat.tools import handle_get_assignment_details
1579
  logger.info(f"Tool: get_assignment_details(assignment_id='{assignment_id}', order_id='{order_id}', driver_id='{driver_id}')")
1580
+
1581
+ # STEP 1: Authenticate user
1582
+ user = get_authenticated_user()
1583
+ if not user:
1584
+ return {"success": False, "error": "Authentication required"}
1585
+
1586
+ # STEP 2: Check permissions
1587
+ required_scope = get_required_scope('get_assignment_details')
1588
+ if not check_permission(user.get('scopes', []), required_scope):
1589
+ return {"success": False, "error": "Permission denied"}
1590
+
1591
+ # STEP 3: Execute with user_id
1592
  return handle_get_assignment_details({
1593
  "assignment_id": assignment_id,
1594
  "order_id": order_id,
1595
  "driver_id": driver_id
1596
+ }, user_id=user['user_id'])
1597
 
1598
 
1599
  @mcp.tool()
 
1636
  """
1637
  from chat.tools import handle_update_assignment
1638
  logger.info(f"Tool: update_assignment(assignment_id='{assignment_id}', status='{status}')")
1639
+
1640
+ # STEP 1: Authenticate user
1641
+ user = get_authenticated_user()
1642
+ if not user:
1643
+ return {"success": False, "error": "Authentication required"}
1644
+
1645
+ # STEP 2: Check permissions
1646
+ required_scope = get_required_scope('update_assignment')
1647
+ if not check_permission(user.get('scopes', []), required_scope):
1648
+ return {"success": False, "error": "Permission denied"}
1649
+
1650
+ # STEP 3: Execute with user_id
1651
  return handle_update_assignment({
1652
  "assignment_id": assignment_id,
1653
  "status": status,
1654
  "actual_arrival": actual_arrival,
1655
  "actual_distance_meters": actual_distance_meters,
1656
  "notes": notes
1657
+ }, user_id=user['user_id'])
1658
 
1659
 
1660
  @mcp.tool()
 
1687
  """
1688
  from chat.tools import handle_unassign_order
1689
  logger.info(f"Tool: unassign_order(assignment_id='{assignment_id}', confirm={confirm})")
1690
+
1691
+ # STEP 1: Authenticate user
1692
+ user = get_authenticated_user()
1693
+ if not user:
1694
+ return {"success": False, "error": "Authentication required"}
1695
+
1696
+ # STEP 2: Check permissions
1697
+ required_scope = get_required_scope('unassign_order')
1698
+ if not check_permission(user.get('scopes', []), required_scope):
1699
+ return {"success": False, "error": "Permission denied"}
1700
+
1701
+ # STEP 3: Execute with user_id
1702
+ return handle_unassign_order({"assignment_id": assignment_id, "confirm": confirm}, user_id=user['user_id'])
1703
 
1704
 
1705
  @mcp.tool()
 
1749
  """
1750
  from chat.tools import handle_complete_delivery
1751
  logger.info(f"Tool: complete_delivery(assignment_id='{assignment_id}', confirm={confirm})")
1752
+
1753
+ # STEP 1: Authenticate user
1754
+ user = get_authenticated_user()
1755
+ if not user:
1756
+ return {"success": False, "error": "Authentication required"}
1757
+
1758
+ # STEP 2: Check permissions
1759
+ required_scope = get_required_scope('complete_delivery')
1760
+ if not check_permission(user.get('scopes', []), required_scope):
1761
+ return {"success": False, "error": "Permission denied"}
1762
+
1763
+ # STEP 3: Execute with user_id
1764
  return handle_complete_delivery({
1765
  "assignment_id": assignment_id,
1766
  "confirm": confirm,
1767
  "actual_distance_meters": actual_distance_meters,
1768
  "notes": notes
1769
+ }, user_id=user['user_id'])
1770
 
1771
 
1772
  @mcp.tool()
 
1835
  """
1836
  from chat.tools import handle_fail_delivery
1837
  logger.info(f"Tool: fail_delivery(assignment_id='{assignment_id}', reason='{failure_reason}')")
1838
+
1839
+ # STEP 1: Authenticate user
1840
+ user = get_authenticated_user()
1841
+ if not user:
1842
+ return {"success": False, "error": "Authentication required"}
1843
+
1844
+ # STEP 2: Check permissions
1845
+ required_scope = get_required_scope('fail_delivery')
1846
+ if not check_permission(user.get('scopes', []), required_scope):
1847
+ return {"success": False, "error": "Permission denied"}
1848
+
1849
+ # STEP 3: Execute with user_id
1850
  return handle_fail_delivery({
1851
  "assignment_id": assignment_id,
1852
  "current_lat": current_lat,
 
1854
  "failure_reason": failure_reason,
1855
  "confirm": confirm,
1856
  "notes": notes
1857
+ }, user_id=user['user_id'])
1858
 
1859
 
1860
  # ============================================================================
test_oauth.html ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>FleetMind OAuth Test - Stytch Login</title>
7
+ <style>
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
10
+ max-width: 800px;
11
+ margin: 50px auto;
12
+ padding: 20px;
13
+ background: #0f172a;
14
+ color: #e2e8f0;
15
+ }
16
+ .container {
17
+ background: #1e293b;
18
+ padding: 40px;
19
+ border-radius: 12px;
20
+ box-shadow: 0 8px 16px rgba(0,0,0,0.4);
21
+ }
22
+ h1 { color: #f1f5f9; }
23
+ h2 { color: #e2e8f0; border-bottom: 2px solid #334155; padding-bottom: 10px; }
24
+ button {
25
+ background: #3b82f6;
26
+ color: white;
27
+ border: none;
28
+ padding: 12px 24px;
29
+ border-radius: 6px;
30
+ font-size: 16px;
31
+ cursor: pointer;
32
+ margin: 10px 5px;
33
+ }
34
+ button:hover { background: #2563eb; }
35
+ .success { background: #10b981; padding: 15px; border-radius: 6px; margin: 15px 0; }
36
+ .error { background: #ef4444; padding: 15px; border-radius: 6px; margin: 15px 0; }
37
+ .info { background: #1e3a5f; padding: 15px; border-radius: 6px; margin: 15px 0; border-left: 4px solid #3b82f6; }
38
+ code {
39
+ background: #334155;
40
+ color: #60a5fa;
41
+ padding: 3px 8px;
42
+ border-radius: 4px;
43
+ font-family: 'Courier New', monospace;
44
+ display: block;
45
+ margin: 10px 0;
46
+ word-wrap: break-word;
47
+ }
48
+ input {
49
+ width: 100%;
50
+ padding: 10px;
51
+ margin: 10px 0;
52
+ border-radius: 6px;
53
+ border: 1px solid #334155;
54
+ background: #0f172a;
55
+ color: #e2e8f0;
56
+ font-size: 14px;
57
+ }
58
+ #result { margin-top: 20px; }
59
+ </style>
60
+ <script src="https://js.stytch.com/stytch.js"></script>
61
+ </head>
62
+ <body>
63
+ <div class="container">
64
+ <h1>πŸ” FleetMind OAuth Test</h1>
65
+ <p>Test Stytch authentication flow locally</p>
66
+
67
+ <div class="info">
68
+ <strong>πŸ“‹ Test Flow:</strong><br>
69
+ 1. Enter your email below<br>
70
+ 2. Click "Send Magic Link"<br>
71
+ 3. Check your email inbox<br>
72
+ 4. Click the magic link<br>
73
+ 5. You'll be redirected back here with a session token<br>
74
+ 6. Use that token to test the MCP tools
75
+ </div>
76
+
77
+ <h2>Step 1: Login with Stytch</h2>
78
+ <div id="loginSection">
79
+ <input type="email" id="emailInput" placeholder="Enter your email address" value="">
80
+ <button onclick="sendMagicLink()">πŸ“§ Send Magic Link</button>
81
+ </div>
82
+
83
+ <h2>Step 2: Session Token</h2>
84
+ <div id="tokenSection">
85
+ <p>After clicking the magic link, your session token will appear here:</p>
86
+ <code id="tokenDisplay">Waiting for login...</code>
87
+ <button onclick="copyToken()" id="copyBtn" style="display:none;">πŸ“‹ Copy Token</button>
88
+ </div>
89
+
90
+ <h2>Step 3: Test MCP Tools</h2>
91
+ <div id="testSection">
92
+ <p>Once you have a token, test calling an MCP tool:</p>
93
+ <button onclick="testCountOrders()" id="testBtn" disabled>πŸ§ͺ Test count_orders Tool</button>
94
+ <button onclick="testCreateOrder()" id="createBtn" disabled>βž• Test create_order Tool</button>
95
+ </div>
96
+
97
+ <div id="result"></div>
98
+ </div>
99
+
100
+ <script>
101
+ const STYTCH_PUBLIC_TOKEN = 'public-token-test-ce2e3056-c04f-4416-a853-dd26810e14db'; // Your Stytch public token
102
+ const SERVER_URL = 'http://localhost:7860';
103
+
104
+ let sessionToken = null;
105
+ let stytchClient = null;
106
+
107
+ // Initialize Stytch
108
+ window.onload = function() {
109
+ stytchClient = window.Stytch(STYTCH_PUBLIC_TOKEN);
110
+
111
+ // Check if we're being redirected back from Stytch
112
+ const urlParams = new URLSearchParams(window.location.search);
113
+ const token = urlParams.get('stytch_token_type');
114
+ const tokenValue = urlParams.get('token');
115
+
116
+ if (token === 'magic_links' && tokenValue) {
117
+ authenticateToken(tokenValue);
118
+ }
119
+ };
120
+
121
+ async function sendMagicLink() {
122
+ const email = document.getElementById('emailInput').value;
123
+ if (!email) {
124
+ showResult('Please enter an email address', 'error');
125
+ return;
126
+ }
127
+
128
+ showResult('Sending magic link to ' + email + '...', 'info');
129
+
130
+ try {
131
+ const response = await fetch('https://test.stytch.com/v1/magic_links/email/login_or_create', {
132
+ method: 'POST',
133
+ headers: {
134
+ 'Content-Type': 'application/json',
135
+ },
136
+ body: JSON.stringify({
137
+ email: email,
138
+ login_magic_link_url: window.location.href,
139
+ signup_magic_link_url: window.location.href,
140
+ public_token: STYTCH_PUBLIC_TOKEN
141
+ })
142
+ });
143
+
144
+ const data = await response.json();
145
+
146
+ if (response.ok) {
147
+ showResult('βœ… Magic link sent! Check your email (' + email + ') and click the link.', 'success');
148
+ } else {
149
+ showResult('❌ Error: ' + (data.error_message || 'Failed to send magic link'), 'error');
150
+ }
151
+ } catch (error) {
152
+ showResult('❌ Network error: ' + error.message, 'error');
153
+ }
154
+ }
155
+
156
+ async function authenticateToken(token) {
157
+ showResult('Authenticating your magic link token...', 'info');
158
+
159
+ try {
160
+ const response = await fetch('https://test.stytch.com/v1/magic_links/authenticate', {
161
+ method: 'POST',
162
+ headers: {
163
+ 'Content-Type': 'application/json',
164
+ },
165
+ body: JSON.stringify({
166
+ token: token,
167
+ public_token: STYTCH_PUBLIC_TOKEN
168
+ })
169
+ });
170
+
171
+ const data = await response.json();
172
+
173
+ if (response.ok && data.session_token) {
174
+ sessionToken = data.session_token;
175
+ document.getElementById('tokenDisplay').textContent = sessionToken;
176
+ document.getElementById('copyBtn').style.display = 'inline-block';
177
+ document.getElementById('testBtn').disabled = false;
178
+ document.getElementById('createBtn').disabled = false;
179
+ showResult('βœ… Login successful! You can now test the MCP tools below.', 'success');
180
+ } else {
181
+ showResult('❌ Authentication failed: ' + (data.error_message || 'Unknown error'), 'error');
182
+ }
183
+ } catch (error) {
184
+ showResult('❌ Authentication error: ' + error.message, 'error');
185
+ }
186
+ }
187
+
188
+ function copyToken() {
189
+ navigator.clipboard.writeText(sessionToken);
190
+ showResult('βœ… Token copied to clipboard!', 'success');
191
+ }
192
+
193
+ async function testCountOrders() {
194
+ if (!sessionToken) {
195
+ showResult('❌ Please login first to get a session token', 'error');
196
+ return;
197
+ }
198
+
199
+ showResult('Testing count_orders with your session token...', 'info');
200
+
201
+ try {
202
+ // Note: This is a simplified test - actual MCP protocol is more complex
203
+ const response = await fetch(SERVER_URL + '/test-auth', {
204
+ method: 'POST',
205
+ headers: {
206
+ 'Authorization': 'Bearer ' + sessionToken,
207
+ 'Content-Type': 'application/json',
208
+ },
209
+ body: JSON.stringify({
210
+ tool: 'count_orders',
211
+ params: {}
212
+ })
213
+ });
214
+
215
+ const data = await response.json();
216
+ showResult('Response: ' + JSON.stringify(data, null, 2), 'success');
217
+ } catch (error) {
218
+ showResult('Note: Direct HTTP testing not available. Use the token in Claude Desktop instead.\n\nYour token: ' + sessionToken, 'info');
219
+ }
220
+ }
221
+
222
+ async function testCreateOrder() {
223
+ if (!sessionToken) {
224
+ showResult('❌ Please login first to get a session token', 'error');
225
+ return;
226
+ }
227
+
228
+ showResult('Testing create_order with your session token...', 'info');
229
+ showResult('Note: For full testing, configure Claude Desktop to use this token.\n\nYour session token:\n' + sessionToken, 'info');
230
+ }
231
+
232
+ function showResult(message, type) {
233
+ const resultDiv = document.getElementById('result');
234
+ resultDiv.className = type;
235
+ resultDiv.innerHTML = message.replace(/\n/g, '<br>');
236
+ }
237
+ </script>
238
+ </body>
239
+ </html>