Dynamic Tool Loading System Architecture in mcp-airflow-api
The mcp-airflow-api repository implements a runtime dynamic tool loading system that detects the target Airflow API version from environment variables and injects version-specific request handlers into a shared core of 43 tools, enabling a single codebase to serve both Airflow 2.x and 3.x deployments.
The call518/mcp-airflow-api project provides a Model Context Protocol (MCP) server for Apache Airflow, designed to operate across major Airflow versions without maintaining separate codebases. At the heart of this flexibility lies a dynamic tool loading system that adapts the server's toolset at startup based on the detected Airflow API version, swapping in the correct request handling logic while reusing a common implementation layer.
Three-Layer Architecture Overview
The dynamic tool loading system consists of three coordinated layers that execute sequentially during server initialization. This architecture separates version detection from tool registration, allowing the system to remain extensible for future Airflow API versions.
Version Detection Layer
The loading process begins in src/mcp_airflow_api/functions.py, where the get_api_version() function reads the AIRFLOW_API_VERSION environment variable (defaulting to v1) and normalizes it to lowercase. This simple string value—either v1 for Airflow 2.x compatibility or v2 for Airflow 3.x—drives the entire downstream registration process.
# From functions.py
def get_api_version() -> str:
return os.getenv("AIRFLOW_API_VERSION", "v1").lower()
Version-Specific Registration Modules
Based on the detected version, the system loads one of two registration modules located in src/mcp_airflow_api/tools/v1_tools.py or src/mcp_airflow_api/tools/v2_tools.py. Each module exports a register_tools(mcp) function that performs three critical operations:
- Import shared implementations from
common_tools.py - Inject the version-specific request helper by assigning either
airflow_request_v1orairflow_request_v2tocommon_tools.airflow_request - Register the core toolset by calling
common_tools.register_common_tools(mcp), which adds 43 standard tools to the MCP instance
The v2 module performs an additional step: it registers two extra asset-management tools that exist only in the Airflow 3.x API surface.
# Conceptual flow inside v2_tools.py
import common_tools
from functions import airflow_request_v2
def register_tools(mcp):
common_tools.airflow_request = airflow_request_v2 # Injection
common_tools.register_common_tools(mcp) # 43 core tools
# Add v2-specific tools
@mcp.tool()
async def list_assets(...): ...
Bootstrap Orchestration
The entry point in src/mcp_airflow_api/mcp_main.py contains the create_mcp_server() function, which orchestrates the dynamic loading. This function instantiates a FastMCP object, queries get_api_version(), imports the appropriate registration module, and executes its register_tools method. The returned server instance contains a toolset that exactly matches the target Airflow API version.
# From mcp_main.py
def create_mcp_server() -> FastMCP:
api_version = get_api_version()
mcp_instance = FastMCP("mcp-airflow-api")
if api_version == "v1":
from mcp_airflow_api.tools import v1_tools
v1_tools.register_tools(mcp_instance)
elif api_version == "v2":
from mcp_airflow_api.tools import v2_tools
v2_tools.register_tools(mcp_instance)
else:
raise ValueError("Unsupported API version")
return mcp_instance
The Injection Mechanism
The architecture uses a dependency injection pattern to avoid code duplication across API versions. Instead of passing request helpers as arguments to every tool function, the system employs module-level attribute assignment.
In src/mcp_airflow_api/tools/common_tools.py, a placeholder airflow_request variable is defined at module scope. During registration, the version-specific module overwrites this variable with the appropriate implementation. When the 43 core tools execute, they reference common_tools.airflow_request, which now points to the correct versioned function.
# Inside common_tools.py
airflow_request = None # Injected at registration time
@mcp.tool()
async def list_dags(...):
# Uses the version-specific function that was injected
resp = await airflow_request("GET", "/dags")
return resp
This approach allows common_tools.py to remain completely agnostic of HTTP implementation details, while the version-specific modules handle the peculiarities of Airflow 2.x versus 3.x REST endpoints.
Configuration and Deployment
To activate the dynamic tool loading system for a specific environment, set the environment variable before invoking the server:
# Target Airflow 2.x (v1 API)
export AIRFLOW_API_VERSION=v1
python -m mcp_airflow_api
# Target Airflow 3.x (v2 API)
export AIRFLOW_API_VERSION=v2
python -m mcp_airflow_api
The system validates the version string during bootstrap and raises a ValueError for unsupported values, preventing silent misconfiguration.
Summary
- Environment-driven detection: The
get_api_version()function infunctions.pyreadsAIRFLOW_API_VERSIONto determine the target API (v1 or v2). - Pluggable registration:
v1_tools.pyandv2_tools.pyeach provide aregister_tools()function that injects the correct request handler and registers version-appropriate tools. - Shared core:
common_tools.pycontains 43 tool implementations that rely on the injectedairflow_requestvariable, eliminating code duplication. - Startup binding: Tool selection happens once during
create_mcp_server()execution inmcp_main.py, resulting in a static but version-correct toolset for the server's lifetime.
Frequently Asked Questions
How does the dynamic tool loading system determine which Airflow version to target?
The system checks the AIRFLOW_API_VERSION environment variable via functions.get_api_version(). Valid values are v1 (default) for Airflow 2.x compatibility and v2 for Airflow 3.x. The value is normalized to lowercase and used to select the appropriate registration module during server initialization.
What happens to the core tools when the API version changes?
The 43 core tools defined in common_tools.py remain unchanged. The dependency injection mechanism swaps only the airflow_request function reference. When v1_tools.register_tools() runs, it assigns the v1 request helper; when v2_tools.register_tools() runs, it assigns the v2 helper. The tools themselves call the injected function without knowing which version is active.
Can I add tools that exist only in one Airflow version?
Yes. Place version-specific tool definitions in the respective registration module. For Airflow 3.x exclusives, add the tool decorator and function to src/mcp_airflow_api/tools/v2_tools.py after the call to register_common_tools(). The v1 module will not load these tools, keeping the v1 toolset clean and preventing errors against Airflow 2.x endpoints.
Is the tool selection hot-swappable without restarting the server?
No. The dynamic tool loading system executes once at startup inside create_mcp_server(). After the FastMCP instance is configured and returned, the toolset is fixed for that process lifetime. To change API versions, you must restart the server with a different AIRFLOW_API_VERSION value.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →