unifi-mcp-tool-builder — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited unifi-mcp-tool-builder (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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:
Before implementation, clarify:
Tool Categories in UniFi MCP Server:
Load UniFi API reference:
# Read the comprehensive UniFi API documentation
Read: docs/UNIFI_API.mdThis document contains verified endpoints for UniFi Network v10.0.156+.
Critical considerations:
Study similar tools in the codebase:
# 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.pyKey patterns to identify:
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:
docs/UNIFI_API.md verification notesKnown endpoint issues:
Document your plan covering:
Tool Definition:
{action}_{resource})Data Models:
Testing Strategy:
Documentation:
Location: src/models/{feature}.py
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: strKey requirements:
model_config instead of Config)extra="forbid" to reject unknown fieldsLocation: src/tools/{category}.py (or new file if new category)
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
@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.
Detailed explanation of the tool's purpose, when to use it,
and what it accomplishes. Include examples of common use cases.
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)
Returns:
Dictionary containing:
- success: Operation success status
- resource_id: Modified resource identifier
- message: Human-readable result message
- details: (if dry_run) Preview of changes
Raises:
ValueError: If site_id is invalid or resource not found
PermissionError: If confirm=True not provided for mutation
Examples:
>>> # Read-only operation
>>> result = await your_tool_name(site_id="default", resource_id="abc123")
>>> # 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)
# Get client from context
client: UniFiClient = settings.get("client")
# 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."
)
# 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."
}
# 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 {})},
)
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
raiseImplementation checklist:
@mcp.tool() decoratorsrc/utils/validators.py)Location: src/main.py
Add your tool to the imports and ensure it's registered:
from src.tools.your_category import your_tool_name
# Tools are auto-registered via @mcp.tool() decorator
# No manual registration needed with FastMCPRun quality checks:
# 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.pyLocation: tests/unit/test_{category}.py
CRITICAL: UniFi MCP Server requires Test-Driven Development (TDD)
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
@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"}
}
result = await your_tool_name(
site_id="default",
resource_id="abc123",
settings=context,
confirm=True
)
assert result["success"] is True
assert result["resource_id"] == "abc123"
client.request.assert_called_once()
@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.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
)
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
with pytest.raises(ValueError, match="Invalid site"):
await your_tool_name(
site_id="",
resource_id="abc123",
settings=context
)
@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")
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"}}
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"Test coverage requirements:
Target: 85%+ coverage for new code
# 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 reportLocation: API.md
Add your tool to the appropriate section with complete documentation:
### 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**:{ "success": true, "resource_id": "abc123", "message": "Successfully processed resource", "data": { / resource details / } }
**Dry-run response**:
{ "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**:
result = await mcp.call_tool("your_tool_name", { "site_id": "default", "resource_id": "abc123", "dry_run": True })
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)
**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)
If adding a new feature category, update README.md:
### Your New Feature Category
- **Feature Name**: Description of what it enables
- **Tools**: List of new tools
- **Use Cases**: Common scenarios where this is valuableAdd entry under "Unreleased" section:
## [Unreleased]
### Added
- New tool `your_tool_name` for managing XYZ (#123)
- Support for ABC feature in UniFi Network 10.0+# Run all tests
pytest tests/unit/
# Verify no regressions
pytest tests/unit/ --cov=src --cov-report=term-missing
# Check overall coverage (should remain ≥78%)
pytest tests/unit/ --cov=src --cov-report=html# 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# Start MCP Inspector
uv run mcp dev src/main.py
# Open http://localhost:5173
# Test your new tool with real inputs
# Verify responses match expectationsIf you have access to a UniFi controller:
Before submitting:
{action}_{resource})black formatting passesisort import sorting passesruff linting passesmypy type checking passesbandit security scan passespytest all tests pass@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", [])@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@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
}@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", [])
})
return {"sites": results}README.md - Project overview and quick startAPI.md - Complete MCP tool referenceAGENTS.md - AI agent development guidelinesDEVELOPMENT_PLAN.md - Roadmap and feature planningTESTING_PLAN.md - Testing strategy and coverage goalsCONTRIBUTING.md - Contribution guidelinesdocs/UNIFI_API.md - Verified endpoint documentation for v10.0.156+src/tools/devices.py - Device management examplessrc/tools/firewall.py - Firewall rule management patternssrc/tools/zbf_tools.py - Zone-Based Firewall implementationsrc/tools/qos.py - QoS profile management examplestests/unit/test_topology.py - High-coverage test examples (95%+)Last Updated: 2026-01-25 Maintained By: UniFi MCP Server Team
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.