Skip to content

Module Development

This guide covers implementing new modules for the Falcon MCP Server.

Each module:

  1. Inherits from BaseModule
  2. Implements register_tools()
  3. Defines tool methods that interact with the Falcon API
  4. Uses common utilities for error handling and API interactions

Create falcon_mcp/modules/your_module.py:

"""Your module for Falcon MCP Server."""
from typing import Any
from mcp.server import FastMCP
from mcp.types import ToolAnnotations
from pydantic import Field
from falcon_mcp.common.logging import get_logger
from falcon_mcp.modules.base import BaseModule
logger = get_logger(__name__)
class YourModule(BaseModule):
"""Module for [description]."""
def register_tools(self, server: FastMCP) -> None:
# Read-only tool — default annotations apply automatically
self._add_tool(
server=server,
method=self.your_search_method,
name="your_search_name",
)
# Mutating tool — must override annotations
self._add_tool(
server=server,
method=self.your_create_method,
name="your_create_name",
annotations=ToolAnnotations(
readOnlyHint=False,
destructiveHint=False,
idempotentHint=False,
openWorldHint=True,
),
)
def your_search_method(
self,
filter: str | None = Field(
default=None,
description="FQL filter string.",
),
limit: int = Field(default=100, ge=1, le=500),
) -> list[dict[str, Any]]:
"""Search for entities. Tool descriptions come from docstrings."""
ids = self._base_search("QueryOperation", filter=filter, limit=limit)
if self._is_error(ids):
return [ids]
if ids:
details = self._base_get_by_ids("GetOperation", ids)
if self._is_error(details):
return [details]
return details
return []

Modules are automatically discovered — no manual imports or registration needed. The registry scans falcon_mcp/modules/, finds classes ending in Module, and registers them.

All tools default to READ_ONLY_ANNOTATIONS. Override for mutating tools:

Tool TypereadOnlyHintdestructiveHintidempotentHint
Search/Get/ListTrueFalseTrue
Create/WriteFalseFalseFalse
Delete/RemoveFalseTrueTrue
Launch/TriggerFalseFalseFalse

Search tools MUST return full entity details, not just IDs. Always follow the two-step pattern:

# Step 1: Query for IDs
ids = self._base_search("QueryDevicesByFilter", filter=filter, limit=limit)
if self._is_error(ids):
return [ids]
# Step 2: Fetch full details
if ids:
return self._base_get_by_ids("PostDeviceDetailsV2", ids)
return []

Create tests/modules/test_your_module.py inheriting from TestModules:

from falcon_mcp.modules.your_module import YourModule
from tests.modules.utils.test_modules import TestModules
class TestYourModule(TestModules):
def setUp(self):
self.setup_module(YourModule)
def test_register_tools(self):
self.assert_tools_registered(["falcon_your_search_name"])
def test_search_returns_details(self):
mock_response = {"status_code": 200, "body": {"resources": ["id1"]}}
self.mock_client.command.return_value = mock_response
result = self.module.your_search_method()
# verify result contains full details, not IDs

Use modern Python 3.10+ syntax:

# ✅ Correct
def search(filter: str | None = None) -> list[dict[str, Any]]:
# ❌ Avoid
from typing import Optional, List, Dict
def search(filter: Optional[str] = None) -> List[Dict[str, Any]]:
ModuleUtility
falcon_mcp.common.errorshandle_api_response, is_success_response
falcon_mcp.common.utilsprepare_api_parameters, extract_resources
falcon_mcp.common.loggingget_logger
falcon_mcp.common.api_scopesget_required_scopes