unifi-mcp-tool-builder
Skill294 starsChanged 18 days agoApache-2.0View on GitHub
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.
- Reads credentials
We split this skill into the rules it gives an agent
12 rules. About 0 are common, 11 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈11
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 63
Endpoint availability: Not all documented endpoints exist in all versions
- Distinctive: line 101
Test on real hardware: if possible (U7 Express, UDM Pro, etc.)
- Distinctive: line 112
Document your plan covering:
- Very specific: line 177
Use Pydantic v2 syntax (model_config instead of Config)
- Distinctive: line 313
Write failing tests first
- Distinctive: line 411
Add your tool to the appropriate section with complete documentation:
- Distinctive: line 488
If adding a new feature category, update README.md:
- Distinctive: line 500
Add entry under "Unreleased" section:
- Distinctive: line 560
If you have access to a UniFi controller:
- Distinctive: line 563
Test tool with real data
- Distinctive: line 564
Verify API endpoint exists and returns expected format
- Distinctive: line 565
Document any discrepancies in API.md
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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:
- Device Management (list, control, upgrade)
- Client Management (block, unblock, statistics)
- Network Configuration (VLANs, subnets, DHCP)
- Firewall & Security (rules, zones, ACLs)
- WiFi/SSID Management (create, configure, statistics)
- QoS & Traffic (profiles, flows, DPI)
- Backup & Restore (automated backups, restore)
- Site Management (multi-site, aggregation)
- Topology & Monitoring (network maps, health)
1.2 Study UniFi API Documentation
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:
- 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:
# 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:
- 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:
- Check endpoint existence in
docs/UNIFI_API.mdverification notes - Test on real hardware if possible (U7 Express, UDM Pro, etc.)
- 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
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:
- Use Pydantic v2 syntax (
model_configinstead ofConfig) 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 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:
from typing import Any
from ..api.client import UniFiClient
from ..config import Settings
from ..utils import ValidationError, get_logger, sanitize_log_message, validate_site_id
async def list_your_resources(site_id: str, settings: Settings) -> list[dict[str, Any]]:
"""List your resources on a site.
Args:
site_id: Site identifier (name or UUID)
settings: Application settings
Returns:
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]A write tool:
from ..utils.validators import coerce_bool, validate_confirmation
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.
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
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")
if coerce_bool(dry_run):
return {"dry_run": True, "operation": "update_your_resource", "payload": payload}
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 responseBuild 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:
- Public
async defwithsettings: Settings; helpers start with_ - Google-style docstring that lists every argument (MCP clients show it)
- Input validation with
src/utils/validators.py - Write tools:
confirmanddry_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(ValidationErrorfor bad input); no broadexceptthat hides the cause
2.3 Register the Module
Location: src/main.py
- A new function in an existing module needs no registration.
- A new module: import it, add it to
_LOCAL_TOOL_MODULESor_CLOUD_TOOL_MODULES, and add it to theUNIFI_PROFILEsets that should include it. - Regenerate the risk-tier snapshot:
UPDATE_TOOL_TIERS=1 pytest tests/unit/fleet/test_tiers.py. Destructive tools need adelete_,remove_orbulk_delete_prefix or an entry inDESTRUCTIVE_TOOLS(src/tool_registry.py). A write tool with a risky word in its name needs an entry inDESTRUCTIVE_TOOLSorRISKY_NAME_EXEMPTIONS.
2.4 Follow Code Quality Standards
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.pyPhase 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)
- Write failing tests first
- Implement just enough to make tests pass
- Refactor and improve
- Achieve 85%+ coverage for new code
3.2 Test Structure
Location: tests/unit/tools/test_{module}_tools.py
from unittest.mock import AsyncMock, patch
import pytest
from src.config import Settings
from src.tools.your_category import update_your_resource
from src.utils.exceptions import ValidationError
@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()
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
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"}]})
await update_your_resource("default", "abc", settings, name="new", confirm=True)
assert client.put.await_args.kwargs["json_data"] == {"name": "new"}
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()
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
# 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 reportPhase 4: Documentation
4.1 Update API.md
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:
ValidationError: Invalid input, or confirm=True missing on a writeResourceNotFoundError: 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 valuable4.3 Update CHANGELOG.md
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+Phase 5: Quality Verification
5.1 Run Full Test Suite
# Run all tests
pytest tests/unit/
# Verify no regressions
pytest tests/unit/ --cov=src --cov-report=term-missing
# Check overall coverage (the CI gate is 80%)
pytest tests/unit/ --cov=src --cov-report=html5.2 Code Quality Checks
# 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-files5.3 Manual Testing with MCP Inspector
# Start MCP Inspector
uv run mcp dev src/main.py
# Open http://localhost:5173
# Test your new tool with real inputs
# Verify responses match expectations5.4 Test on Real Hardware (if possible)
If you have access to a UniFi controller:
- Configure local gateway API credentials
- Test tool with real data
- Verify API endpoint exists and returns expected format
- 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
blackformatting passesisortimport sorting passesrufflinting passesmypytype checking passesbanditsecurity scan passespytestall tests pass- Coverage ≥80% overall (CI gate), ≥85% for new code
- Pre-commit hooks pass
- MCP Inspector manual testing successful
Common Patterns in UniFi MCP Server
Copy a real tool rather than a sketch:
| 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 startAPI.md- Complete MCP tool referenceAGENTS.md- AI agent development guidelinesDEVELOPMENT_PLAN.md- Roadmap and feature planningdocs/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
Code Examples
src/tools/devices.py- Device management examplessrc/tools/firewall.py- Firewall rule management patternssrc/tools/firewall_policies.py- Zone-Based Firewall policiessrc/tools/qos.py- QoS profile management examplestests/unit/tools/test_protect_relays.py- Write-gate and dry-run test examples
Last Updated: 2026-10-13 Maintained By: UniFi MCP Server Team
In this repository52 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .agents/skills/fastmcp/CLAUDE.mdCLAUDE.md
- CLAUDE.mdCLAUDE.md
- .claude/skills/fastmcp/CLAUDE.mdCLAUDE.md
- .cursor/rules/common-mistakes.mdcCursor rule
- .cursor/rules/core-principles.mdcCursor rule
- .cursor/rules/environment-setup.mdcCursor rule
- .cursor/rules/mcp-tools.mdcCursor rule
- .cursor/rules/project-context.mdcCursor rule
- .cursor/rules/README.mdCursor rule
- .cursor/rules/unifi-api.mdcCursor rule
- .cursor/rules/workflow.mdcCursor rule
- FastMCP DevelopmentSkill
- mcp-builderSkill
- source-command-build-dockerSkill
- source-command-check-depsSkill
- source-command-cleanSkill
- source-command-docsSkill
- source-command-formatSkill
- source-command-lintSkill
- source-command-prSkill
- source-command-securitySkill
- source-command-setupSkill
- source-command-testSkill
- source-command-unifi-api-docs-update-2Skill
- source-command-unifi-api-docs-updateSkill
- source-command-unifi-mcp-add-toolSkill
- source-command-unifi-mcp-release-prepSkill
- source-command-unifi-mcp-test-coverageSkill
- source-command-unifi-mcp-update-docsSkill
- unifi-cloud-fleet-toolsSkill
- unifi-network-config-toolsSkill
- unifi-protect-toolsSkill
- unifi-security-toolsSkill
- FastMCP DevelopmentSkill
- mcp-builderSkill
- unifi-cloud-fleet-toolsSkill
- unifi-mcp-tool-builderSkill
- unifi-network-config-toolsSkill
- unifi-protect-toolsSkill
- unifi-security-toolsSkill
- unifiSkill
- unifi-channel-planningSkill
- unifi-cloud-fleet-toolsSkill
- unifi-devicesSkill
- unifi-mcp-tool-builderSkill
- unifi-network-config-toolsSkill
- unifi-networkSkill
- unifi-protect-toolsSkill
- unifi-securitySkill
- unifi-security-toolsSkill
- unifi-systemSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

