unifi-mcp-tool-builder · diff
v1.0.0 to v1.0.0
133 added, 321 removed. Audit A to A.
---
name: unifi-mcp-tool-builder
description: Specialized guide for adding new MCP tools to the UniFi MCP Server following project standards, UniFi API patterns, and test-driven development practices. Use when implementing new UniFi Network Controller features as MCP tools.
license: Apache 2.0
version: 1.0.0
author: UniFi MCP Server Team
---
# UniFi MCP Tool Builder
## Overview
This skill guides you through adding new MCP tools to the existing UniFi MCP Server. Unlike building an MCP server from scratch, this focuses on **extending** the current 74-tool implementation with new UniFi Network Controller functionality while maintaining the project's quality standards.
**Project Context:**
- **Existing Tools**: 74 MCP tools across 9 feature categories
- **Test Coverage**: 990 tests, 78.18% overall coverage
- **Architecture**: FastMCP + Pydantic v2 + async/await patterns
- **API Support**: Local gateway (full), Cloud V1/EA (limited)
- **Quality Standards**: 80% minimum test coverage, TDD required
---
# Process
## Phase 1: Research and Planning
### 1.1 Understand the Tool Request
Before implementation, clarify:
- **What UniFi feature/endpoint** needs to be exposed?
- **Which API mode** supports it (local, cloud-v1, cloud-ea)?
- **What workflow** does this enable for AI agents?
- **How does this fit** into existing tool categories?
**Tool Categories in UniFi MCP Server:**
1. Device Management (list, control, upgrade)
2. Client Management (block, unblock, statistics)
3. Network Configuration (VLANs, subnets, DHCP)
4. Firewall & Security (rules, zones, ACLs)
5. WiFi/SSID Management (create, configure, statistics)
6. QoS & Traffic (profiles, flows, DPI)
7. Backup & Restore (automated backups, restore)
8. Site Management (multi-site, aggregation)
9. Topology & Monitoring (network maps, health)
### 1.2 Study UniFi API Documentation
**Load UniFi API reference:**
```bash
# Read the comprehensive UniFi API documentation
Read: docs/UNIFI_API.md
```
This document contains verified endpoints for UniFi Network v10.0.156+.
**Critical considerations:**
- **Endpoint availability**: Not all documented endpoints exist in all versions
- **API mode differences**: Local gateway vs Cloud APIs have different capabilities
- **Response formats**: Local API returns different structures than Cloud API
- **Authentication**: Local uses local credentials, Cloud uses API keys
### 1.3 Review Existing Patterns
**Study similar tools in the codebase:**
```bash
# Example: For a new device tool, read existing device tools
Read: src/tools/devices.py
Read: src/tools/device_control.py
# For network tools:
Read: src/tools/networks.py
Read: src/tools/network_config.py
# For testing patterns:
Read: tests/unit/test_devices.py
```
**Key patterns to identify:**
- How similar tools structure their inputs (Pydantic models)
- Response formatting (JSON with TypedDict or Pydantic response models)
- Error handling patterns
- Confirmation requirements for mutating operations
- Dry-run mode implementation
- Caching strategies (if applicable)
### 1.4 Verify API Endpoint Availability
**CRITICAL: Not all documented API endpoints actually exist!**
The UniFi MCP Server has discovered that many documented endpoints don't exist in real controllers. Before implementation:
1. **Check endpoint existence** in `docs/UNIFI_API.md` verification notes
2. **Test on real hardware** if possible (U7 Express, UDM Pro, etc.)
3. **Document findings** in code comments and API.md
**Known endpoint issues:**
- ZBF matrix endpoints don't exist (use Firewall Policies v2 instead)
- Some statistics endpoints are documented but not available
- Cloud APIs have very limited endpoint support vs local gateway
### 1.5 Create Implementation Plan
Document your plan covering:
**Tool Definition:**
- Tool name (follow naming convention: `{action}_{resource}`)
- Description (one-line summary for LLMs)
- Input parameters (with Pydantic model)
- Response format (TypedDict or Pydantic model)
- Error scenarios
**Data Models:**
- Pydantic models for request validation
- Response models (if complex data structure)
- Enums for allowed values
**Testing Strategy:**
- Unit test scenarios (minimum 10-15 tests)
- Mock API responses
- Edge cases (empty results, errors, pagination)
- Target coverage: 85%+ for new code
**Documentation:**
- Docstring with examples
- API.md section update
- README.md feature list update (if major feature)
---
## Phase 2: Implementation
### 2.1 Create Pydantic Models (if needed)
**Location**: `src/models/{feature}.py`
```python
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field, ConfigDict
class YourRequestModel(BaseModel):
"""Request model for your_tool operation.
Attributes:
site_id: UniFi site identifier
resource_id: Resource to operate on
options: Optional configuration parameters
"""
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
site_id: str = Field(..., description="UniFi site ID (e.g., 'default')")
resource_id: str = Field(..., min_length=1, description="Resource identifier")
options: Optional[dict] = Field(None, description="Additional options")
class YourResponseModel(BaseModel):
"""Response from your_tool operation."""
success: bool
resource_id: str
message: str
```
**Key requirements:**
- Use Pydantic v2 syntax (`model_config` instead of `Config`)
- `extra="forbid"` to reject unknown fields
- Comprehensive Field descriptions for LLM understanding
- Type hints for all fields
- Docstrings explaining purpose
### 2.2 Implement the MCP Tool
- **Location**: `src/tools/{category}.py` (or new file if new category)
+ **Location**: `src/tools/{category}.py` (or a new module for a new feature area)
+ Tools are plain async functions. Every public `async def` in a module that `src/main.py` lists becomes a tool; there is no `@mcp.tool()` decorator. The registry passes `settings` and hides it from the tool's public signature. Name helpers with a leading `_` so they are not registered.
+
+ A read tool:
+
```python
- from fastmcp import Context
- from src.models.your_model import YourRequestModel, YourResponseModel
- from src.api.client import UniFiClient
- from src.utils.validators import validate_site_id
+ from typing import Any
- @mcp.tool()
- async def your_tool_name(
- site_id: str,
- resource_id: str,
- options: dict | None = None,
- settings: Context = None,
- confirm: bool = False,
- dry_run: bool = False,
- ) -> dict:
- """Brief one-line description of what this tool does.
+ from ..api.client import UniFiClient
+ from ..config import Settings
+ from ..utils import ValidationError, get_logger, sanitize_log_message, validate_site_id
- Detailed explanation of the tool's purpose, when to use it,
- and what it accomplishes. Include examples of common use cases.
+ async def list_your_resources(site_id: str, settings: Settings) -> list[dict[str, Any]]:
+ """List your resources on a site.
+
Args:
- site_id: UniFi site identifier (e.g., 'default')
- resource_id: Resource to operate on
- options: Optional configuration dictionary
- settings: FastMCP context (auto-injected)
- confirm: Required for mutating operations (default: False)
- dry_run: Preview changes without applying (default: False)
+ site_id: Site identifier (name or UUID)
+ settings: Application settings
Returns:
- Dictionary containing:
- - success: Operation success status
- - resource_id: Modified resource identifier
- - message: Human-readable result message
- - details: (if dry_run) Preview of changes
+ List of resource records
+ """
+ site_id = validate_site_id(site_id)
+ logger = get_logger(__name__, settings.log_level)
+ async with UniFiClient(settings) as client:
+ if not client.is_authenticated:
+ await client.authenticate()
+ response = await client.get(f"/ea/sites/{site_id}/rest/your_resource")
+ data = response if isinstance(response, list) else response.get("data", [])
+ logger.info(sanitize_log_message(f"Listed {len(data)} resources for site {site_id}"))
+ return [YourModel(**item).model_dump() for item in data]
+ ```
- Raises:
- ValueError: If site_id is invalid or resource not found
- PermissionError: If confirm=True not provided for mutation
+ A write tool:
- Examples:
- >>> # Read-only operation
- >>> result = await your_tool_name(site_id="default", resource_id="abc123")
+ ```python
+ from ..utils.validators import coerce_bool, validate_confirmation
- >>> # Mutating operation (requires confirmation)
- >>> result = await your_tool_name(
- ... site_id="default",
- ... resource_id="abc123",
- ... confirm=True
- ... )
- >>> # Preview changes first
- >>> preview = await your_tool_name(
- ... site_id="default",
- ... resource_id="abc123",
- ... dry_run=True
- ... )
- """
- # Validate inputs
- validate_site_id(site_id)
+ async def update_your_resource(
+ site_id: str,
+ resource_id: str,
+ settings: Settings,
+ name: str | None = None,
+ confirm: bool | str = False,
+ dry_run: bool | str = False,
+ ) -> dict[str, Any]:
+ """Update a resource.
- # Get client from context
- client: UniFiClient = settings.get("client")
+ Args:
+ site_id: Site identifier (name or UUID)
+ resource_id: Resource identifier
+ settings: Application settings
+ name: New name (unchanged when omitted)
+ confirm: Must be true to apply the change (required unless dry_run)
+ dry_run: Preview the change without applying it
- # MUTATING OPERATIONS: Require confirmation
- if not dry_run:
- if not confirm:
- raise PermissionError(
- "This operation modifies network configuration. "
- "Set confirm=True to proceed, or use dry_run=True to preview changes."
- )
+ Returns:
+ The updated record, or the planned change on a dry run
+ """
+ validate_confirmation(confirm, "update your resource", dry_run)
+ site_id = validate_site_id(site_id)
+ payload: dict[str, Any] = {}
+ if name is not None:
+ payload["name"] = name
+ if not payload:
+ raise ValidationError("No updates provided")
- # Dry-run mode: Preview changes
- if dry_run:
- return {
- "success": True,
- "dry_run": True,
- "preview": {
- "operation": "your_operation",
- "resource_id": resource_id,
- "changes": {"key": "value"},
- },
- "message": "Dry-run completed. Set confirm=True to apply changes."
- }
+ if coerce_bool(dry_run):
+ return {"dry_run": True, "operation": "update_your_resource", "payload": payload}
- # Execute operation
- try:
- response = await client.request(
- method="POST",
- endpoint=f"/api/s/{site_id}/rest/your_endpoint",
- json={"resource_id": resource_id, **(options or {})},
+ async with UniFiClient(settings) as client:
+ if not client.is_authenticated:
+ await client.authenticate()
+ response = await client.put(
+ f"/ea/sites/{site_id}/rest/your_resource/{resource_id}", json_data=payload
)
-
- return {
- "success": True,
- "resource_id": resource_id,
- "message": f"Successfully processed resource {resource_id}",
- "data": response.get("data", {}),
- }
-
- except Exception as e:
- # Provide actionable error messages
- if "not found" in str(e).lower():
- raise ValueError(
- f"Resource '{resource_id}' not found in site '{site_id}'. "
- f"Use list_resources tool to see available resources."
- ) from e
- raise
+ return response
```
+ Build paths with the `Settings` helpers (`get_integration_path`, `get_protect_integration_path`, `get_v2_api_path`) or the `/ea/sites/{site_id}/...` form that `UniFiClient` translates. Never hardcode `/proxy/network/...`. The tool-set skills (`unifi-protect-tools`, `unifi-network-config-tools`, `unifi-security-tools`, `unifi-cloud-fleet-tools`) list the paths and gotchas per area.
+
**Implementation checklist:**
- - [ ] FastMCP `@mcp.tool()` decorator
- - [ ] Comprehensive docstring with examples
- - [ ] Input validation (use existing validators in `src/utils/validators.py`)
- - [ ] Confirm requirement for mutating operations
- - [ ] Dry-run mode support for previewing changes
- - [ ] Actionable error messages that guide LLM to correct usage
- - [ ] Async/await for all I/O operations
- - [ ] Proper exception handling with context
+ - [ ] Public `async def` with `settings: Settings`; helpers start with `_`
+ - [ ] Google-style docstring that lists every argument (MCP clients show it)
+ - [ ] Input validation with `src/utils/validators.py`
+ - [ ] Write tools: `confirm` and `dry_run`, `validate_confirmation()` first, dry run returned before any request
+ - [ ] Optional update arguments default to `None`
+ - [ ] No audit code in the tool (the registry wrapper audits every write)
+ - [ ] Errors from `src/utils/exceptions.py` (`ValidationError` for bad input); no broad `except` that hides the cause
- ### 2.3 Register Tool in Main Server
+ ### 2.3 Register the Module
**Location**: `src/main.py`
- Add your tool to the imports and ensure it's registered:
-
- ```python
- from src.tools.your_category import your_tool_name
-
- # Tools are auto-registered via @mcp.tool() decorator
- # No manual registration needed with FastMCP
- ```
+ - A new function in an existing module needs no registration.
+ - A new module: import it, add it to `_LOCAL_TOOL_MODULES` or `_CLOUD_TOOL_MODULES`, and add it to the `UNIFI_PROFILE` sets that should include it.
+ - Regenerate the risk-tier snapshot: `UPDATE_TOOL_TIERS=1 pytest tests/unit/fleet/test_tiers.py`. Destructive tools need a `delete_`, `remove_` or `bulk_delete_` prefix or an entry in `DESTRUCTIVE_TOOLS` (`src/tool_registry.py`). A write tool with a risky word in its name needs an entry in `DESTRUCTIVE_TOOLS` or `RISKY_NAME_EXEMPTIONS`.
### 2.4 Follow Code Quality Standards
**Run quality checks:**
```bash
# Format code
black src/tools/your_file.py
isort src/tools/your_file.py
# Lint
ruff check src/tools/your_file.py --fix
# Type check
mypy src/tools/your_file.py
```
---
## Phase 3: Test-Driven Development
### 3.1 Write Tests BEFORE Implementation
**Location**: `tests/unit/test_{category}.py`
**CRITICAL: UniFi MCP Server requires Test-Driven Development (TDD)**
1. Write failing tests first
2. Implement just enough to make tests pass
3. Refactor and improve
4. Achieve 85%+ coverage for new code
### 3.2 Test Structure
- ```python
- import pytest
- from unittest.mock import AsyncMock, MagicMock, patch
- from src.tools.your_category import your_tool_name
-
- class TestYourToolName:
- """Test suite for your_tool_name."""
-
- @pytest.fixture
- def mock_context(self):
- """Mock FastMCP context with UniFi client."""
- context = MagicMock()
- client = AsyncMock()
- context.get.return_value = client
- return context, client
+ **Location**: `tests/unit/tools/test_{module}_tools.py`
- @pytest.mark.asyncio
- async def test_successful_operation(self, mock_context):
- """Test successful tool execution."""
- context, client = mock_context
- client.request.return_value = {
- "data": {"_id": "abc123", "name": "test"}
- }
+ ```python
+ from unittest.mock import AsyncMock, patch
- result = await your_tool_name(
- site_id="default",
- resource_id="abc123",
- settings=context,
- confirm=True
- )
+ import pytest
- assert result["success"] is True
- assert result["resource_id"] == "abc123"
- client.request.assert_called_once()
+ from src.config import Settings
+ from src.tools.your_category import update_your_resource
+ from src.utils.exceptions import ValidationError
- @pytest.mark.asyncio
- async def test_requires_confirmation(self, mock_context):
- """Test that mutating operations require confirm=True."""
- context, client = mock_context
- with pytest.raises(PermissionError, match="Set confirm=True"):
- await your_tool_name(
- site_id="default",
- resource_id="abc123",
- settings=context,
- confirm=False
- )
+ @pytest.fixture
+ def settings(monkeypatch):
+ monkeypatch.setenv("UNIFI_API_KEY", "test-api-key")
+ monkeypatch.setenv("UNIFI_API_TYPE", "local")
+ monkeypatch.setenv("UNIFI_LOCAL_HOST", "192.168.2.1")
+ return Settings()
- @pytest.mark.asyncio
- async def test_dry_run_mode(self, mock_context):
- """Test dry-run preview mode."""
- context, client = mock_context
- result = await your_tool_name(
- site_id="default",
- resource_id="abc123",
- settings=context,
- dry_run=True
- )
+ def _client(client_class, **responses):
+ client = AsyncMock()
+ client.is_authenticated = True
+ for method, value in responses.items():
+ setattr(client, method, AsyncMock(return_value=value))
+ client.__aenter__ = AsyncMock(return_value=client)
+ client.__aexit__ = AsyncMock(return_value=False) # a truthy value would swallow errors
+ client_class.return_value = client
+ return client
- assert result["dry_run"] is True
- assert "preview" in result
- client.request.assert_not_called()
- @pytest.mark.asyncio
- async def test_invalid_site_id(self, mock_context):
- """Test validation of site_id."""
- context, client = mock_context
+ async def test_update_sends_only_changed_fields(settings):
+ with patch("src.tools.your_category.UniFiClient") as client_class:
+ client = _client(client_class, put={"data": [{"_id": "abc", "name": "new"}]})
- with pytest.raises(ValueError, match="Invalid site"):
- await your_tool_name(
- site_id="",
- resource_id="abc123",
- settings=context
- )
+ await update_your_resource("default", "abc", settings, name="new", confirm=True)
- @pytest.mark.asyncio
- async def test_resource_not_found(self, mock_context):
- """Test handling of not found errors."""
- context, client = mock_context
- client.request.side_effect = Exception("Resource not found")
+ assert client.put.await_args.kwargs["json_data"] == {"name": "new"}
- with pytest.raises(ValueError, match="not found"):
- await your_tool_name(
- site_id="default",
- resource_id="nonexistent",
- settings=context,
- confirm=True
- )
- @pytest.mark.asyncio
- async def test_with_optional_parameters(self, mock_context):
- """Test tool with optional parameters."""
- context, client = mock_context
- client.request.return_value = {"data": {"_id": "abc123"}}
+ async def test_update_refuses_without_confirm(settings):
+ with patch("src.tools.your_category.UniFiClient") as client_class:
+ with pytest.raises(ValidationError, match="confirm"):
+ await update_your_resource("default", "abc", settings, name="new")
+ client_class.assert_not_called()
- result = await your_tool_name(
- site_id="default",
- resource_id="abc123",
- options={"key": "value"},
- settings=context,
- confirm=True
- )
- assert result["success"] is True
- # Verify options were passed to API
- call_args = client.request.call_args
- assert call_args[1]["json"]["key"] == "value"
+ async def test_update_dry_run_makes_no_request(settings):
+ with patch("src.tools.your_category.UniFiClient") as client_class:
+ result = await update_your_resource("default", "abc", settings, name="new", dry_run=True)
+ assert result["dry_run"] is True
+ client_class.assert_not_called()
```
+ `asyncio_mode = auto` is set, so tests need no asyncio marker. `--strict-markers` rejects any marker except `integration`.
+
**Test coverage requirements:**
- Successful operation (happy path)
- Confirmation requirement (for mutating ops)
- Dry-run mode
- Input validation (invalid site_id, etc.)
- Error handling (not found, API errors)
- Optional parameters
- Edge cases (empty results, pagination)
- Response format validation
**Target**: 85%+ coverage for new code
### 3.3 Run Tests
```bash
# Run your specific test file
pytest tests/unit/test_your_category.py -v
# Run with coverage
pytest tests/unit/test_your_category.py --cov=src/tools/your_category --cov-report=term-missing
# Ensure coverage meets target (85%+)
pytest tests/unit/test_your_category.py --cov=src/tools/your_category --cov-report=html
open htmlcov/index.html # Review coverage report
```
---
## Phase 4: Documentation
### 4.1 Update API.md
**Location**: `API.md`
Add your tool to the appropriate section with complete documentation:
```markdown
### your_tool_name
**Description**: Brief one-line description
**Purpose**: Detailed explanation of when and why to use this tool
**Parameters**:
- `site_id` (string, required): UniFi site identifier (e.g., "default")
- `resource_id` (string, required): Resource identifier to operate on
- `options` (object, optional): Additional configuration options
- `confirm` (boolean, optional): Required for mutating operations (default: false)
- `dry_run` (boolean, optional): Preview changes without applying (default: false)
**Returns**:
```json
{
"success": true,
"resource_id": "abc123",
"message": "Successfully processed resource",
"data": { /* resource details */ }
}
```
**Dry-run response**:
```json
{
"success": true,
"dry_run": true,
"preview": {
"operation": "your_operation",
"resource_id": "abc123",
"changes": { /* preview of changes */ }
},
"message": "Dry-run completed. Set confirm=True to apply changes."
}
```
**Examples**:
```python
# Example 1: Preview changes (dry-run)
result = await mcp.call_tool("your_tool_name", {
"site_id": "default",
"resource_id": "abc123",
"dry_run": True
})
# Example 2: Execute operation
result = await mcp.call_tool("your_tool_name", {
"site_id": "default",
"resource_id": "abc123",
"confirm": True
})
```
**Error Handling**:
- - `ValueError`: Invalid site_id or resource not found
- - `PermissionError`: Mutating operation requires confirm=True
- - `APIError`: UniFi API returned error (check message for details)
+ - `ValidationError`: Invalid input, or confirm=True missing on a write
+ - `ResourceNotFoundError`: The resource does not exist (HTTP 404)
+ - `APIError`: UniFi API returned another error (check the message)
**API Endpoint**: `POST /api/s/{site}/rest/your_endpoint`
**Supported API Modes**:
- ✅ Local Gateway API (full support)
- ⚠️ Cloud V1 API (limited support)
- ❌ Cloud EA API (not supported)
```
### 4.2 Update README.md (if major feature)
If adding a new feature category, update README.md:
```markdown
### Your New Feature Category
- **Feature Name**: Description of what it enables
- **Tools**: List of new tools
- **Use Cases**: Common scenarios where this is valuable
```
### 4.3 Update CHANGELOG.md
Add entry under "Unreleased" section:
```markdown
## [Unreleased]
### Added
- New tool `your_tool_name` for managing XYZ (#123)
- Support for ABC feature in UniFi Network 10.0+
```
---
## Phase 5: Quality Verification
### 5.1 Run Full Test Suite
```bash
# Run all tests
pytest tests/unit/
# Verify no regressions
pytest tests/unit/ --cov=src --cov-report=term-missing
- # Check overall coverage (should remain ≥78%)
+ # Check overall coverage (the CI gate is 80%)
pytest tests/unit/ --cov=src --cov-report=html
```
### 5.2 Code Quality Checks
```bash
# Format (auto-fix)
black src/ tests/
isort src/ tests/
# Lint (auto-fix where possible)
ruff check src/ tests/ --fix
# Type check
mypy src/
# Security scan
bandit -r src/
# Pre-commit hooks
pre-commit run --all-files
```
### 5.3 Manual Testing with MCP Inspector
```bash
# Start MCP Inspector
uv run mcp dev src/main.py
# Open http://localhost:5173
# Test your new tool with real inputs
# Verify responses match expectations
```
### 5.4 Test on Real Hardware (if possible)
If you have access to a UniFi controller:
1. Configure local gateway API credentials
2. Test tool with real data
3. Verify API endpoint exists and returns expected format
4. Document any discrepancies in API.md
---
## Quality Checklist
Before submitting:
### Code Quality
- [ ] Tool follows naming convention (`{action}_{resource}`)
- [ ] Comprehensive docstring with examples
- [ ] Input validation using Pydantic models
- [ ] Confirmation required for mutating operations
- [ ] Dry-run mode implemented
- [ ] Actionable error messages
- [ ] Async/await used consistently
- [ ] Type hints throughout
- [ ] No hardcoded values (use constants)
- [ ] Follows existing code patterns
### Testing
- [ ] TDD: Tests written before implementation
- [ ] 85%+ coverage for new code
- [ ] Tests cover happy path
- [ ] Tests cover error scenarios
- [ ] Tests cover edge cases
- [ ] Mock API responses properly
- [ ] No integration tests without real hardware
- [ ] All tests passing
### Documentation
- [ ] API.md updated with complete tool documentation
- [ ] Examples provided in docstring and API.md
- [ ] Error handling documented
- [ ] API endpoint documented
- [ ] API mode support clearly indicated
- [ ] CHANGELOG.md updated
- [ ] README.md updated (if major feature)
### Integration
- [ ] Tool registered in src/main.py
- [ ] Imports organized correctly
- [ ] No circular dependencies
- [ ] Compatible with existing tools
### Quality Gates
- [ ] `black` formatting passes
- [ ] `isort` import sorting passes
- [ ] `ruff` linting passes
- [ ] `mypy` type checking passes
- [ ] `bandit` security scan passes
- [ ] `pytest` all tests pass
- - [ ] Coverage ≥78% overall, ≥85% for new code
+ - [ ] Coverage ≥80% overall (CI gate), ≥85% for new code
- [ ] Pre-commit hooks pass
- [ ] MCP Inspector manual testing successful
---
## Common Patterns in UniFi MCP Server
- ### Pattern 1: List/Query Tools (Read-Only)
-
- ```python
- @mcp.tool()
- async def list_resources(
- site_id: str,
- filters: dict | None = None,
- settings: Context = None,
- ) -> list[dict]:
- """List resources in the UniFi controller.
-
- This is a read-only operation requiring no confirmation.
- """
- client = settings.get("client")
- response = await client.request(
- method="GET",
- endpoint=f"/api/s/{site_id}/rest/resource",
- params=filters or {}
- )
- return response.get("data", [])
- ```
-
- ### Pattern 2: Mutating Tools with Confirmation
-
- ```python
- @mcp.tool()
- async def update_resource(
- site_id: str,
- resource_id: str,
- updates: dict,
- settings: Context = None,
- confirm: bool = False,
- dry_run: bool = False,
- ) -> dict:
- """Update a resource (requires confirmation).
-
- Mutating operations always require confirm=True.
- """
- if not dry_run and not confirm:
- raise PermissionError("Set confirm=True to proceed")
-
- if dry_run:
- return {"dry_run": True, "preview": updates}
-
- client = settings.get("client")
- response = await client.request(
- method="PUT",
- endpoint=f"/api/s/{site_id}/rest/resource/{resource_id}",
- json=updates
- )
- return response
- ```
-
- ### Pattern 3: Tools with Pagination
-
- ```python
- @mcp.tool()
- async def list_large_dataset(
- site_id: str,
- limit: int = 100,
- offset: int = 0,
- settings: Context = None,
- ) -> dict:
- """List resources with pagination support."""
- client = settings.get("client")
- response = await client.request(
- method="GET",
- endpoint=f"/api/s/{site_id}/rest/resource",
- params={"limit": limit, "offset": offset}
- )
-
- return {
- "data": response.get("data", []),
- "total": response.get("meta", {}).get("total", 0),
- "limit": limit,
- "offset": offset
- }
- ```
-
- ### Pattern 4: Multi-Site Aggregation
-
- ```python
- @mcp.tool()
- async def aggregate_across_sites(
- settings: Context = None,
- ) -> dict:
- """Aggregate data across all sites."""
- client = settings.get("client")
-
- # Get all sites
- sites_response = await client.request(
- method="GET",
- endpoint="/api/self/sites"
- )
- sites = sites_response.get("data", [])
-
- # Aggregate data
- results = []
- for site in sites:
- site_id = site["name"]
- site_data = await client.request(
- method="GET",
- endpoint=f"/api/s/{site_id}/rest/resource"
- )
- results.append({
- "site_id": site_id,
- "data": site_data.get("data", [])
- })
+ Copy a real tool rather than a sketch:
- return {"sites": results}
- ```
+ | Pattern | Copy from |
+ |---------|-----------|
+ | Read tool, legacy API | `list_wlans` in `src/tools/wifi.py` |
+ | Read tool, integration API | `list_acl_rules` in `src/tools/acls.py` |
+ | Write tool with a partial update | `update_protect_relay` in `src/tools/protect_relays.py` |
+ | Read-modify-write update | `update_port_profile` in `src/tools/port_profiles.py` |
+ | Paginated list | `list_protect_relays` in `src/tools/protect_relays.py` (uses `validate_limit_offset()`) |
+ | Data across many controllers | `fleet_fanout` in `src/fleet/fleet_tools.py` (reads only) |
---
## References
### Project Documentation
- `README.md` - Project overview and quick start
- `API.md` - Complete MCP tool reference
- `AGENTS.md` - AI agent development guidelines
- `DEVELOPMENT_PLAN.md` - Roadmap and feature planning
- - `TESTING_PLAN.md` - Testing strategy and coverage goals
+ - `docs/archive/TESTING_PLAN.md` - Testing strategy (archived)
- `CONTRIBUTING.md` - Contribution guidelines
### UniFi API Documentation
- `docs/UNIFI_API.md` - Verified endpoint documentation for v10.0.156+
- [UniFi Network Developer Documentation](https://developer.ui.com/)
### Code Examples
- `src/tools/devices.py` - Device management examples
- `src/tools/firewall.py` - Firewall rule management patterns
- - `src/tools/zbf_tools.py` - Zone-Based Firewall implementation
+ - `src/tools/firewall_policies.py` - Zone-Based Firewall policies
- `src/tools/qos.py` - QoS profile management examples
- - `tests/unit/test_topology.py` - High-coverage test examples (95%+)
+ - `tests/unit/tools/test_protect_relays.py` - Write-gate and dry-run test examples
---
- **Last Updated**: 2026-01-25
+ **Last Updated**: 2026-10-13
**Maintained By**: UniFi MCP Server Team