# Architecture Flow Diagram
## š Complete Request Flow
```
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā CLIENT (Laravel Frontend) ā
ā ā
ā User clicks "Ask AI" ā Sends message + Bearer Token ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā POST /chat
ā Authorization: Bearer <token>
ā {"message": "Show my contracts"}
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā WEB CHAT API (FastAPI) ā
ā src/web_chat/main.py ā
ā ā
ā 1. Extract Bearer token from header ā
ā 2. Call get_user_from_token(token) ā
ā āā Check cache (15 min TTL) ā
ā āā If cached: return user data (10ms) ā
ā āā If not: call Laravel API (150ms) ā
ā ā
ā 3. Laravel API: GET /api/get-login-user-data ā
ā Returns: ā
ā { ā
ā "user_id": 123, ā
ā "role_id": 10, // Dealership Admin ā
ā "organization_id": 1, ā
ā "dealership_id": 5, ā
ā "email": "user@example.com", ā
ā "permissions": ["contract.index", "contract.create", ...] ā
ā } ā
ā ā
ā 4. Create UserContext object ā
ā UserContext( ā
ā user_id=123, ā
ā role=Role.DEALERSHIP_ADMIN, ā
ā bearer_token=token, ā
ā organization_id=1, ā
ā dealership_id=5, ā
ā permissions=[...] ā
ā ) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā process_query(message, user_context)
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ORCHESTRATOR (Query Processor) ā
ā src/orchestrator/processor.py ā
ā ā
ā 1. Connect to MCP Server ā
ā 2. Get available tools from MCP ā
ā 3. Filter tools by RBAC ā
ā āā User role: DEALERSHIP_ADMIN ā
ā āā Available: DEALERSHIP_TOOLS + AUTHENTICATED_TOOLS ā
ā āā Filtered: ["get_dealership_contracts", "get_user_profile", ...] ā
ā ā
ā 4. Call LLM with filtered tools ā
ā āā Provider: OpenAI/Claude/Gemini ā
ā āā Message: "Show my contracts" ā
ā āā Tools: [filtered tool list] ā
ā ā
ā 5. LLM decides to call: get_dealership_contracts() ā
ā ā
ā 6. Execute tool via MCP ā
ā āā Inject user context into tool arguments ā
ā āā args["user_id"] = 123 ā
ā āā args["organization_id"] = 1 ā
ā āā args["dealership_id"] = 5 ā
ā āā args["bearer_token"] = token ā
ā āā args["permissions"] = [...] ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā call_tool("get_dealership_contracts", args)
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā MCP SERVER (FastMCP) ā
ā src/mcp_server/server.py ā
ā ā
ā 1. Receive tool call with injected context ā
ā 2. Route to registered tool ā
ā 3. Call tool function with arguments ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā get_dealership_contracts(user_context, ...)
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā TOOL (Your Implementation) ā
ā src/mcp_server/tools/contracts/get_contracts.py ā
ā ā
ā 1. Receive UserContext ā
ā user_context = UserContext( ā
ā user_id=123, ā
ā role=DEALERSHIP_ADMIN, ā
ā bearer_token=token, ā
ā dealership_id=5, ā
ā permissions=["contract.index", ...] ā
ā ) ā
ā ā
ā 2. Check permissions ā
ā if "contract.index" not in user_context.permissions: ā
ā return error("Missing permission") ā
ā ā
ā 3. Call Laravel API ā
ā GET /api/contracts ā
ā Headers: {"Authorization": f"Bearer {user_context.bearer_token}"} ā
ā ā
ā 4. Laravel returns contracts ā
ā { ā
ā "data": [ ā
ā {"id": 1, "vendor": "ABC", "dealership_id": 5, ...}, ā
ā {"id": 2, "vendor": "XYZ", "dealership_id": 5, ...}, ā
ā {"id": 3, "vendor": "DEF", "dealership_id": 8, ...} ā Other ā
ā ] ā
ā } ā
ā ā
ā 5. Filter by hierarchy ā
ā filtered = RBAC.filter_data_by_hierarchy( ā
ā data=contracts, ā
ā user_context=user_context ā
ā ) ā
ā Result: Only contracts where dealership_id=5 ā
ā ā
ā 6. Return envelope response ā
ā wrap_response( ā
ā tool_name="get_dealership_contracts", ā
ā data={"contracts": filtered, "count": 2} ā
ā ) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā Return tool result
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ORCHESTRATOR (Continued) ā
ā ā
ā 7. Receive tool result ā
ā 8. Send result back to LLM ā
ā 9. LLM generates natural language response ā
ā "You have 2 contracts: ā
ā 1. ABC Corp - expires 2024-12-31 ā
ā 2. XYZ Inc - expires 2025-06-30" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā Return final answer
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā WEB CHAT API (Response) ā
ā ā
ā 10. Save conversation history ā
ā 11. Return response to client ā
ā { ā
ā "success": true, ā
ā "response": "You have 2 contracts: ...", ā
ā "history": [...] ā
ā } ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā HTTP Response
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā CLIENT (Display Response) ā
ā ā
ā User sees: "You have 2 contracts: ..." ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
```
---
## š RBAC Filtering at Different Stages
```
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā RBAC FILTERING STAGES ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Stage 1: TOOL VISIBILITY (Orchestrator)
āā Input: All MCP tools
āā Filter: RBAC.is_tool_allowed(user_role, tool_name)
āā Output: Only tools user's role can access
āā Effect: LLM only sees allowed tools
Example:
Dealership Admin sees:
ā
get_dealership_contracts
ā
get_user_profile
ā
upload_contract
ā get_organization_contracts (org-level only)
ā manage_organizations (admin only)
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Stage 2: PERMISSION CHECK (Tool Level)
āā Input: Tool called by LLM
āā Filter: Check user_context.permissions
āā Output: Allow/deny tool execution
āā Effect: Fine-grained permission control
Example:
Tool: upload_contract
Check: "contract.create" in user_context.permissions
Dealership Admin: ā
Has permission ā Proceed
Dealership Viewer: ā No permission ā Return error
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Stage 3: DATA FILTERING (Tool Level)
āā Input: Data from Laravel API
āā Filter: RBAC.filter_data_by_hierarchy(data, user_context)
āā Output: Only data user can see
āā Effect: Hierarchy-aware data access
Example:
Laravel returns 100 contracts:
- 30 from Organization A, Dealership 1
- 40 from Organization A, Dealership 2
- 30 from Organization B, Dealership 3
Org Admin (Org A): Sees 70 contracts (Dealership 1 + 2)
Platform Admin (Platform with Dealership 1): Sees 30 contracts
Dealership Admin (Dealership 1): Sees 30 contracts
Dealership Admin (Dealership 2): Sees 40 contracts
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Stage 4: ACTION FILTERING (Tool Level)
āā Input: User action (create/update/delete)
āā Filter: user_context.can_write / can_manage
āā Output: Allow/deny action
āā Effect: Prevent viewers from modifying data
Example:
Action: Delete contract
Dealership Admin: ā
can_manage=True ā Proceed
Dealership User: ā
can_write=True ā Check permission
Dealership Viewer: ā can_write=False ā Return error
```
---
## šÆ Permission Check Decision Tree
```
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā SHOULD I CHECK PERMISSIONS? ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Tool Type: READ-ONLY (list, get, view)
āā RBAC already filtered tool visibility ā
āā No write/delete actions
āā Decision: NO permission check needed
āā Just filter data by hierarchy
Example: get_dealership_contracts, get_user_profile
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Tool Type: CREATE/UPDATE
āā User might be a viewer (read-only)
āā Need to prevent modifications
āā Decision: YES, check can_write
āā if not user_context.can_write: return error
Example: create_contract, update_contract
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Tool Type: DELETE
āā Sensitive action
āā Only managers/admins should delete
āā Decision: YES, check can_manage + specific permission
āā if not user_context.can_manage: return error
āā if "resource.delete" not in permissions: return error
Example: delete_contract, delete_vendor
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Tool Type: ADMIN ACTION
āā System-wide changes
āā Only admins should access
āā Decision: YES, check is_global_admin or is_organization_level
āā if not user_context.is_global_admin: return error
Example: manage_organizations, system_configuration
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Tool Type: SPECIFIC FEATURE
āā Laravel has specific permission for this
āā Need to check exact permission
āā Decision: YES, check specific Laravel permission
āā if "specific.permission" not in permissions: return error
Example: contracts.renewal, invoice.paid, users.global_filters
```
---
## š Data Flow Example: "Show my contracts"
```
User Message: "Show my contracts"
User Role: Dealership Admin (role_id=10)
Dealership: ID 5
Organization: ID 1
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Step 1: Authentication
āā Bearer token validated
āā User data cached
āā UserContext created
āā user_id: 123
āā role: DEALERSHIP_ADMIN (10)
āā organization_id: 1
āā dealership_id: 5
āā permissions: ["contract.index", "contract.show", ...]
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Step 2: Tool Filtering (RBAC)
āā All MCP tools: 20 tools
āā Filter by role: DEALERSHIP_ADMIN
āā Allowed tools: 8 tools
āā ā
get_dealership_contracts
āā ā
get_dealership_vendors
āā ā
upload_contract
āā ā
get_user_profile
āā ā get_platform_contracts (platform-level)
āā ā get_organization_contracts (org-level)
āā ā manage_organizations (admin-only)
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Step 3: LLM Decision
āā Query: "Show my contracts"
āā Available tools: [8 filtered tools]
āā LLM chooses: get_dealership_contracts()
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Step 4: Tool Execution
āā Tool: get_dealership_contracts
āā Injected context:
ā āā user_id: 123
ā āā organization_id: 1
ā āā dealership_id: 5
ā āā bearer_token: <token>
ā āā permissions: [...]
āā Tool logic:
āā Check permission: "contract.index" ā
āā Call Laravel: GET /api/contracts
āā Laravel returns: 100 contracts (all orgs/dealerships)
āā Filter by hierarchy:
āā Keep: organization_id=1 AND dealership_id=5
āā Result: 12 contracts
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Step 5: LLM Response
āā Tool result: {"contracts": [12 items], "count": 12}
āā LLM generates: "You have 12 contracts for your dealership..."
āā Return to user
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Total Time: ~200ms
āā Auth (cached): 10ms
āā LLM call: 100ms
āā Tool execution: 50ms
āā Response: 40ms
```
---
## š Comparison: Laravel vs MCP RBAC
```
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā LARAVEL APPROACH ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Route::group(['middleware' => ['auth:sanctum', 'permission']], function () {
Route::get('/api/contracts', [ContractController::class, 'index'])
->middleware('permission:contract.index');
Route::post('/api/contracts', [ContractController::class, 'store'])
->middleware('permission:contract.create');
Route::delete('/api/contracts/{id}', [ContractController::class, 'destroy'])
->middleware('permission:contract.delete');
});
Flow:
1. Request hits route
2. auth:sanctum validates token
3. permission middleware checks Laravel permission
4. If allowed ā Controller executes
5. If denied ā 403 Forbidden
Pros:
ā
Simple and explicit
ā
Enforced at route level
ā
Standard Laravel pattern
Cons:
ā Fixed per endpoint
ā No dynamic tool filtering
ā LLM sees all endpoints (can't filter)
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā MCP APPROACH ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
# RBAC at orchestrator level
allowed_tools = RBAC.get_allowed_tools(user_context.role)
# Tool receives full context
@mcp.tool()
async def get_contracts(user_context: UserContext):
# Check permissions
if "contract.index" not in user_context.permissions:
return error("Missing permission")
# Call Laravel API
response = await call_laravel_api(user_context.bearer_token)
# Filter by hierarchy
filtered = RBAC.filter_data_by_hierarchy(data, user_context)
return filtered
Flow:
1. Bearer token validated at API entry
2. RBAC filters tools by role
3. LLM only sees allowed tools
4. Tool checks permissions + filters data
5. Return filtered result
Pros:
ā
Dynamic tool filtering (LLM-friendly)
ā
Hierarchy-aware data filtering
ā
Flexible permission checks
ā
Single auth check (cached)
ā
Context-aware tools
Cons:
ā More complex than middleware
ā Permission checks in tool code
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
VERDICT: MCP approach is BETTER for AI agents because:
1. LLM only sees tools user can actually use
2. Tools can make intelligent decisions based on context
3. Hierarchy filtering is automatic
4. More flexible than fixed routes
5. Better user experience (no "permission denied" errors)
```
---
## š Key Takeaways
1. **Authentication happens once** at API entry (Bearer token)
2. **RBAC filters tools** before LLM sees them
3. **Tools receive full user context** automatically
4. **Permission checks** happen at tool level (flexible)
5. **Data filtering** happens at tool level (hierarchy-aware)
6. **No middleware needed** - MCP architecture is better for AI
**Your current implementation is excellent!** š