# Tool Development Template
## Quick Start Checklist
- [ ] Understand the Laravel endpoint (URL, method, params, response)
- [ ] Identify required Laravel permission (e.g., `contract.index`)
- [ ] Determine hierarchy level (Org, Platform, or Dealership)
- [ ] Create tool directory: `src/mcp_server/tools/your_tool/`
- [ ] Implement tool logic
- [ ] Add tool to RBAC category
- [ ] Test with different roles
---
## ๐ File Structure
```
src/mcp_server/tools/
โโโ your_tool/
โโโ __init__.py # Empty or imports
โโโ tool.py # Main tool implementation
```
---
## ๐ Tool Template
```python
"""
Your Tool - Brief description
This tool provides [functionality] for [user types].
It calls Laravel endpoint: [METHOD] /api/your-endpoint
Required Permission: your.permission
Hierarchy Level: [Organization/Platform/Dealership]
"""
from typing import Dict, Any, Optional
import httpx
from src.core import UserContext, RBAC, wrap_response
from src.config import settings
from src.observability import get_logger
logger = get_logger(__name__)
async def your_tool_logic(
user_context: UserContext,
# Add your tool-specific parameters here
param1: str,
param2: Optional[int] = None,
trace_id: str = "unknown"
) -> Dict[str, Any]:
"""
Main tool logic.
Args:
user_context: User context (auto-injected)
param1: Description of param1
param2: Description of param2
trace_id: Request trace ID
Returns:
Envelope response with data
"""
logger.info(
"your_tool_called",
trace_id=trace_id,
user_id=user_context.user_id,
role=user_context.role.name,
param1=param1
)
try:
# ============================================================
# STEP 1: Permission Checks
# ============================================================
# Option A: Check if user can write (for create/update/delete tools)
if not user_context.can_write:
return wrap_response(
tool_name="your_tool",
trace_id=trace_id,
data={},
missing_reason="Viewer roles cannot perform this action"
)
# Option B: Check specific Laravel permission
required_permission = "your.permission"
if required_permission not in (user_context.permissions or []):
return wrap_response(
tool_name="your_tool",
trace_id=trace_id,
data={},
missing_reason=f"Missing required permission: {required_permission}"
)
# Option C: Check management permission (for admin-only tools)
if not user_context.can_manage:
return wrap_response(
tool_name="your_tool",
trace_id=trace_id,
data={},
missing_reason="Only managers and admins can perform this action"
)
# ============================================================
# STEP 2: Call Laravel API
# ============================================================
# Build request
url = f"{settings.laravel_api_url}/api/your-endpoint"
headers = {
"Authorization": f"Bearer {user_context.bearer_token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
# Option A: GET request
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(
url,
headers=headers,
params={"param1": param1, "param2": param2}
)
response.raise_for_status()
data = response.json()
# Option B: POST request
# async with httpx.AsyncClient(timeout=30.0) as client:
# response = await client.post(
# url,
# headers=headers,
# json={"param1": param1, "param2": param2}
# )
# response.raise_for_status()
# data = response.json()
# ============================================================
# STEP 3: Filter Data by Hierarchy (if needed)
# ============================================================
# If Laravel returns data that needs hierarchy filtering
items = data.get("data", [])
filtered_items = RBAC.filter_data_by_hierarchy(
data=items,
user_context=user_context,
org_field="organization_id",
platform_field="platform_id",
dealership_field="dealership_id"
)
# ============================================================
# STEP 4: Format Response
# ============================================================
logger.info(
"your_tool_completed",
trace_id=trace_id,
user_id=user_context.user_id,
items_count=len(filtered_items)
)
return wrap_response(
tool_name="your_tool",
trace_id=trace_id,
data={
"items": filtered_items,
"total": len(filtered_items),
"message": "Successfully retrieved data"
}
)
except httpx.HTTPStatusError as e:
logger.error(
"your_tool_http_error",
trace_id=trace_id,
status_code=e.response.status_code,
error=str(e)
)
return wrap_response(
tool_name="your_tool",
trace_id=trace_id,
data={},
missing_reason=f"API error: {e.response.status_code} - {e.response.text}"
)
except Exception as e:
logger.error(
"your_tool_failed",
trace_id=trace_id,
error=str(e),
error_type=type(e).__name__
)
return wrap_response(
tool_name="your_tool",
trace_id=trace_id,
data={},
missing_reason=f"Tool execution failed: {str(e)}"
)
# ============================================================
# Tool Registration
# ============================================================
def register(mcp):
"""
Register the tool with MCP server.
This function is called automatically by the auto-discovery system.
"""
@mcp.tool(
name="your_tool",
description=(
"Brief description of what the tool does. "
"Be specific and clear for the LLM. "
"Include examples: 'Use this when user asks to [action]'"
)
)
async def your_tool_wrapper(
# Tool-specific parameters (what LLM provides)
param1: str,
param2: int = None,
# User context parameters (auto-injected by orchestrator)
user_id: int = 0,
organization_id: int = 0,
role: int = 1,
bearer_token: str = "",
platform_id: int = None,
dealership_id: int = None,
email: str = "",
name: str = "",
permissions: list = None,
trace_id: str = "unknown",
) -> Dict[str, Any]:
"""
MCP tool wrapper - receives injected user context.
Args:
param1: Description
param2: Description
[User context params are auto-injected]
Returns:
Envelope response with data
"""
from src.core import Role
# Create UserContext from injected parameters
user_context = UserContext(
user_id=user_id,
role=Role(role),
bearer_token=bearer_token,
organization_id=organization_id,
platform_id=platform_id,
dealership_id=dealership_id,
email=email,
name=name,
permissions=permissions or [],
)
# Call the actual tool logic
return await your_tool_logic(
user_context,
param1,
param2,
trace_id
)
```
---
## ๐ง Add to RBAC
Edit `src/core/security.py`:
```python
class RBAC:
# Choose the appropriate category based on who should access this tool
# For dealership-level tools (all users can access)
DEALERSHIP_TOOLS: Set[str] = {
"get_dealership_contracts",
"your_tool", # โ Add here
}
# For platform-level tools (platform + org users)
PLATFORM_TOOLS: Set[str] = {
"get_platform_contracts",
"your_tool", # โ Or here
}
# For organization-level tools (org users only)
ORGANIZATION_TOOLS: Set[str] = {
"get_organization_contracts",
"your_tool", # โ Or here
}
# For admin-only tools (global admin only)
ADMIN_TOOLS: Set[str] = {
"manage_organizations",
"your_tool", # โ Or here
}
```
---
## ๐งช Testing
### Test with cURL
```bash
# Test as Dealership User
curl -X POST http://localhost:8002/chat \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-d '{
"message": "Use your_tool with param1=test"
}'
# Test as Platform Admin
curl -X POST http://localhost:8002/chat \
-H "Content-Type: application/json" \
-H "Authorization: Bearer PLATFORM_ADMIN_TOKEN" \
-d '{
"message": "Use your_tool with param1=test"
}'
# Test as Org Admin
curl -X POST http://localhost:8002/chat \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ORG_ADMIN_TOKEN" \
-d '{
"message": "Use your_tool with param1=test"
}'
```
### Test Different Scenarios
1. **Test with viewer role** - Should get "read-only" error
2. **Test with missing permission** - Should get "missing permission" error
3. **Test with valid user** - Should return data
4. **Test hierarchy filtering** - Org user should see more than dealership user
---
## ๐ Common Patterns
### Pattern 1: Read-Only Tool (List/Get)
```python
# No write check needed
# Just filter by hierarchy
filtered = RBAC.filter_data_by_hierarchy(data, user_context)
return wrap_response(..., data={"items": filtered})
```
### Pattern 2: Create/Update Tool
```python
# Check write permission
if not user_context.can_write:
return error_response("Viewer roles cannot create/update")
# Check specific permission
if "resource.create" not in user_context.permissions:
return error_response("Missing create permission")
# Proceed with creation
```
### Pattern 3: Delete Tool
```python
# Check management permission
if not user_context.can_manage:
return error_response("Only managers can delete")
# Check specific permission
if "resource.delete" not in user_context.permissions:
return error_response("Missing delete permission")
# Proceed with deletion
```
### Pattern 4: Admin-Only Tool
```python
# Check if global admin or org admin
if not (user_context.is_global_admin or user_context.is_organization_level):
return error_response("Admin access required")
# Proceed with admin action
```
---
## ๐ฏ Permission Check Helper
```python
def has_permission(user_context: UserContext, permission: str) -> bool:
"""Check if user has specific Laravel permission."""
return permission in (user_context.permissions or [])
def has_any_permission(user_context: UserContext, permissions: list[str]) -> bool:
"""Check if user has any of the specified permissions."""
user_perms = set(user_context.permissions or [])
return bool(user_perms.intersection(permissions))
def has_all_permissions(user_context: UserContext, permissions: list[str]) -> bool:
"""Check if user has all of the specified permissions."""
user_perms = set(user_context.permissions or [])
return all(p in user_perms for p in permissions)
```
---
## ๐ Pre-Flight Checklist
Before implementing a tool, answer these:
1. **What Laravel endpoint does it call?**
- URL: _______________
- Method: _______________
2. **What permission is required?**
- Permission: _______________
3. **What hierarchy level?**
- [ ] Organization
- [ ] Platform
- [ ] Dealership
4. **What RBAC category?**
- [ ] PUBLIC_TOOLS
- [ ] AUTHENTICATED_TOOLS
- [ ] DEALERSHIP_TOOLS
- [ ] PLATFORM_TOOLS
- [ ] ORGANIZATION_TOOLS
- [ ] ADMIN_TOOLS
5. **Does it need hierarchy filtering?**
- [ ] Yes (list of items)
- [ ] No (single item or action)
6. **What permission checks?**
- [ ] can_write (for create/update/delete)
- [ ] can_manage (for admin actions)
- [ ] Specific Laravel permission
- [ ] None (read-only, RBAC handles it)
---
## ๐ Ready to Build!
When you provide Laravel endpoints, I'll:
1. โ
Create the tool file
2. โ
Add RBAC entry
3. โ
Implement permission checks
4. โ
Add hierarchy filtering
5. โ
Handle errors properly
6. โ
Add logging
7. โ
Return envelope response
**Just give me:**
- Endpoint URL + method
- Request/response format
- Required permission
- Hierarchy level