astro-airflow-mcp 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- astro_airflow_mcp/__init__.py +5 -0
- astro_airflow_mcp/__main__.py +75 -0
- astro_airflow_mcp/logging.py +61 -0
- astro_airflow_mcp/plugin.py +66 -0
- astro_airflow_mcp/server.py +1374 -0
- astro_airflow_mcp-0.1.5.dist-info/METADATA +231 -0
- astro_airflow_mcp-0.1.5.dist-info/RECORD +10 -0
- astro_airflow_mcp-0.1.5.dist-info/WHEEL +4 -0
- astro_airflow_mcp-0.1.5.dist-info/entry_points.txt +5 -0
- astro_airflow_mcp-0.1.5.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Main entry point for running the Airflow MCP server."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from astro_airflow_mcp.logging import configure_logging, get_logger
|
|
8
|
+
from astro_airflow_mcp.server import configure, mcp
|
|
9
|
+
|
|
10
|
+
logger = get_logger("main")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
"""Main entry point for the Airflow MCP server."""
|
|
15
|
+
# Configure logging
|
|
16
|
+
configure_logging(level=logging.INFO)
|
|
17
|
+
|
|
18
|
+
# Parse command line arguments
|
|
19
|
+
parser = argparse.ArgumentParser(description="Airflow MCP Server")
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--transport",
|
|
22
|
+
type=str,
|
|
23
|
+
default=os.getenv("MCP_TRANSPORT", "http"),
|
|
24
|
+
choices=["stdio", "http"],
|
|
25
|
+
help="Transport mode: http (default) or stdio",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--host",
|
|
29
|
+
type=str,
|
|
30
|
+
default=os.getenv("MCP_HOST", "localhost"),
|
|
31
|
+
help="Host to bind to (only for http transport, default: localhost)",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--port",
|
|
35
|
+
type=int,
|
|
36
|
+
default=int(os.getenv("MCP_PORT", "8000")),
|
|
37
|
+
help="Port to bind to (only for http transport, default: 8000)",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--airflow-url",
|
|
41
|
+
type=str,
|
|
42
|
+
default=os.getenv("AIRFLOW_API_URL", "http://localhost:8080"),
|
|
43
|
+
help="Base URL of Airflow webserver (default: http://localhost:8080)",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--auth-token",
|
|
47
|
+
type=str,
|
|
48
|
+
default=os.getenv("AIRFLOW_AUTH_TOKEN"),
|
|
49
|
+
help="Bearer token for Airflow API authentication",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
args = parser.parse_args()
|
|
53
|
+
|
|
54
|
+
# Configure Airflow connection settings
|
|
55
|
+
configure(
|
|
56
|
+
url=args.airflow_url,
|
|
57
|
+
auth_token=args.auth_token,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Log Airflow connection configuration
|
|
61
|
+
logger.info(f"Airflow URL: {args.airflow_url}")
|
|
62
|
+
if args.auth_token:
|
|
63
|
+
logger.info("Authentication: Bearer token")
|
|
64
|
+
else:
|
|
65
|
+
logger.info("Authentication: None")
|
|
66
|
+
|
|
67
|
+
# Run the server with specified transport
|
|
68
|
+
if args.transport == "http":
|
|
69
|
+
mcp.run(transport="http", host=args.host, port=args.port, show_banner=False)
|
|
70
|
+
else:
|
|
71
|
+
mcp.run(show_banner=False)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Logging utilities for Airflow MCP."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_logger(name: str | None = None) -> logging.Logger:
|
|
8
|
+
"""Get a logger instance nested under the airflow_mcp namespace.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
name: Optional name for the logger. If not provided, returns root airflow_mcp logger.
|
|
12
|
+
Will be nested under 'airflow_mcp' (e.g., 'airflow_mcp.server')
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
A logger instance
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
>>> logger = get_logger("server")
|
|
19
|
+
>>> logger.info("Starting server")
|
|
20
|
+
"""
|
|
21
|
+
if name:
|
|
22
|
+
return logging.getLogger(f"airflow_mcp.{name}")
|
|
23
|
+
return logging.getLogger("airflow_mcp")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def configure_logging(level: str | int = logging.INFO) -> None:
|
|
27
|
+
"""Configure logging for the airflow_mcp package.
|
|
28
|
+
|
|
29
|
+
Sets up a simple console handler with a standard format.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
level: Logging level (e.g., logging.INFO, logging.DEBUG, or "INFO", "DEBUG")
|
|
33
|
+
Defaults to INFO.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> configure_logging(level=logging.DEBUG)
|
|
37
|
+
"""
|
|
38
|
+
# Convert string level to int if needed
|
|
39
|
+
if isinstance(level, str):
|
|
40
|
+
level = getattr(logging, level.upper(), logging.INFO)
|
|
41
|
+
|
|
42
|
+
# Get the root airflow_mcp logger
|
|
43
|
+
logger = get_logger()
|
|
44
|
+
logger.setLevel(level)
|
|
45
|
+
|
|
46
|
+
# Remove any existing handlers to avoid duplicates
|
|
47
|
+
logger.handlers.clear()
|
|
48
|
+
|
|
49
|
+
# Create console handler
|
|
50
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
51
|
+
handler.setLevel(level)
|
|
52
|
+
|
|
53
|
+
# Create simple formatter
|
|
54
|
+
formatter = logging.Formatter(fmt="%(levelname)s: %(message)s")
|
|
55
|
+
handler.setFormatter(formatter)
|
|
56
|
+
|
|
57
|
+
# Add handler to logger
|
|
58
|
+
logger.addHandler(handler)
|
|
59
|
+
|
|
60
|
+
# Prevent propagation to root logger to avoid duplicate logs
|
|
61
|
+
logger.propagate = False
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Airflow plugin for integrating MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from astro_airflow_mcp import __version__
|
|
9
|
+
|
|
10
|
+
# Use standard logging for Airflow plugin integration
|
|
11
|
+
# This allows Airflow to control log level, format, and destination
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from airflow.plugins_manager import AirflowPlugin
|
|
16
|
+
|
|
17
|
+
AIRFLOW_AVAILABLE = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
AIRFLOW_AVAILABLE = False
|
|
20
|
+
AirflowPlugin = object # type: ignore
|
|
21
|
+
logger.warning("Airflow not available, plugin disabled")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# FastAPI app configuration for Airflow 3.x plugin system
|
|
25
|
+
try:
|
|
26
|
+
from fastapi import FastAPI
|
|
27
|
+
|
|
28
|
+
from astro_airflow_mcp.server import mcp
|
|
29
|
+
|
|
30
|
+
# Get the native MCP protocol ASGI app from FastMCP
|
|
31
|
+
mcp_protocol_app = mcp.http_app(path="/")
|
|
32
|
+
|
|
33
|
+
# Wrap in a FastAPI app with the MCP app's lifespan
|
|
34
|
+
# This is required for FastMCP to initialize its task group
|
|
35
|
+
app = FastAPI(
|
|
36
|
+
title="Airflow MCP Server", version=__version__, lifespan=mcp_protocol_app.lifespan
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Mount the MCP protocol app
|
|
40
|
+
app.mount("/v1", mcp_protocol_app)
|
|
41
|
+
logger.info("MCP protocol app created and mounted")
|
|
42
|
+
|
|
43
|
+
# Airflow plugin configuration
|
|
44
|
+
fastapi_apps_config = [{"app": app, "url_prefix": "/mcp", "name": "Airflow MCP Server"}]
|
|
45
|
+
|
|
46
|
+
except ImportError as e:
|
|
47
|
+
logger.warning(f"FastAPI integration not available: {e}")
|
|
48
|
+
fastapi_apps_config = []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AirflowMCPPlugin(AirflowPlugin):
|
|
52
|
+
"""Plugin to integrate MCP server with Airflow.
|
|
53
|
+
|
|
54
|
+
Exposes MCP protocol endpoints at /mcp for AI clients (Cursor, Claude Desktop, etc.)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
name = "astro_airflow_mcp"
|
|
58
|
+
fastapi_apps = fastapi_apps_config
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def on_load(*_args: Any, **_kwargs: Any) -> None:
|
|
62
|
+
"""Called when the plugin is loaded."""
|
|
63
|
+
logger.info("Airflow MCP Plugin loaded")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = ["AirflowMCPPlugin"]
|