Best for
- Use when implementing new UniFi Network Controller features as MCP tools.
enuno/unifi-mcp-server/.agents/skills/unifi-mcp-tool-builder/SKILL.md
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.
Decision brief
Specialized guide for adding new MCP tools to the UniFi MCP Server following project standards, UniFi API patterns, and test-driven development practices.
Compatibility matrix
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/enuno/unifi-mcp-server --skill ".agents/skills/unifi-mcp-tool-builder"Inspect the Agent Skill "unifi-mcp-tool-builder" from https://github.com/enuno/unifi-mcp-server/blob/a34c7dd6a459d964a13316fe7e627f0ad66dad2d/.agents/skills/unifi-mcp-tool-builder/SKILL.md at commit a34c7dd6a459d964a13316fe7e627f0ad66dad2d. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
Before implementation, clarify:
Before implementation, clarify:
Study similar tools in the codebase:
Document your plan covering:
Location: src/models/{feature}.py
Permission review
The documentation includes network, browsing, or remote request actions.
# Open http://localhost:5173Evidence record
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 99/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 238 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
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.md
This 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.py
Key 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: str
Key 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
raise
Implementation 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 FastMCP
Run 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.py
Location: 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 report
Location: 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**:
```json
{
"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:
# 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 foundPermissionError: Mutating operation requires confirm=TrueAPIError: UniFi API returned error (check message for details)API Endpoint: POST /api/s/{site}/rest/your_endpoint
Supported API Modes:
### 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
Add 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 expectations
If 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
Frequently asked questions
Specialized guide for adding new MCP tools to the UniFi MCP Server following project standards, UniFi API patterns, and test-driven development practices.
The source record exposes this install command: npx skills add https://github.com/enuno/unifi-mcp-server --skill ".agents/skills/unifi-mcp-tool-builder". Inspect the command and pinned source before running it.
Static rules flagged network in the source; the page lists the matching lines and excerpts.
Alternatives
mgiovani/cc-arsenal
Multi-agent review team: architecture, security, performance, testing, style, docs/UX, plus an adversary that cross-examines the other 6, for security-sensitive, architectural, or large PRs (15+ files) where a single-agent pass risks missing cross-cutting issues. Use for auth/payments/PII changes, schema/pattern changes, compliance sign-off, or when asked to 'get the review team on this' / 'multi-agent review' / 'thorough review before merge'. For a standard PR or a quick pre-merge check, use /r
th3vib3coder/vibe-science
Scientific research engine for hypothesis testing, literature gap analysis, experimental validation, and data-driven discovery. Enforces adversarial review (Reviewer 2), 32 quality gates, tree search over hypotheses, confounder harness for quantitative claims, and serendipity detection. TRIGGER when: user asks to analyze scientific data, test hypotheses, validate findings, search for research gaps, design experiments, or investigate results. DO NOT TRIGGER when: pure code review, documentation w
Borda/AI-Rig
TDD-first feature development — crystallise API as a demo test, drive implementation to pass it, run quality stack and progressive review loop. TRIGGER when: user asks to build new functionality, add a capability, or implement a feature in a Python project; phrases: "add X", "implement Y", "build Z feature", "create a new module for". SKIP when: bug fixes (use `/develop:fix`); refactoring without new behaviour (use `/develop:refactor`); non-Python projects; `.claude/` config changes (use `/found
upex-galaxy/agentic-qa-boilerplate
Analyze, prioritize, and document test cases in TMS (Jira/Xray), or repair an existing Story-ATS-ATP-ATR-TC cascade through a sealed explicit mode. Use for Test/ATP/ATR artifacts, ROI and automation verdicts, maintaining traceability, fix-traceability, or broken TMS links. The repair-traceability mode audits, plans, waits for explicit approval, applies, and verifies without launching the general documentation workflow. Do NOT use for writing test code (test-automation) or running suites (regress