mcp-mesh 0.4.1__py3-none-any.whl → 0.5.0__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.
- _mcp_mesh/__init__.py +14 -3
- _mcp_mesh/engine/async_mcp_client.py +6 -19
- _mcp_mesh/engine/dependency_injector.py +161 -74
- _mcp_mesh/engine/full_mcp_proxy.py +25 -20
- _mcp_mesh/engine/mcp_client_proxy.py +5 -19
- _mcp_mesh/generated/.openapi-generator/FILES +2 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +2 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +1 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +305 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +1 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +10 -1
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +4 -4
- _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +108 -0
- _mcp_mesh/pipeline/__init__.py +2 -2
- _mcp_mesh/pipeline/api_heartbeat/__init__.py +16 -0
- _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +515 -0
- _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +117 -0
- _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +140 -0
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +247 -0
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +309 -0
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +332 -0
- _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +147 -0
- _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +97 -0
- _mcp_mesh/pipeline/api_startup/__init__.py +20 -0
- _mcp_mesh/pipeline/api_startup/api_pipeline.py +61 -0
- _mcp_mesh/pipeline/api_startup/api_server_setup.py +292 -0
- _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +302 -0
- _mcp_mesh/pipeline/api_startup/route_collection.py +56 -0
- _mcp_mesh/pipeline/api_startup/route_integration.py +318 -0
- _mcp_mesh/pipeline/{startup → mcp_startup}/fastmcpserver_discovery.py +4 -4
- _mcp_mesh/pipeline/{startup → mcp_startup}/heartbeat_loop.py +1 -1
- _mcp_mesh/pipeline/{startup → mcp_startup}/startup_orchestrator.py +170 -5
- _mcp_mesh/shared/config_resolver.py +0 -3
- _mcp_mesh/shared/logging_config.py +2 -1
- _mcp_mesh/shared/sse_parser.py +217 -0
- {mcp_mesh-0.4.1.dist-info → mcp_mesh-0.5.0.dist-info}/METADATA +1 -1
- {mcp_mesh-0.4.1.dist-info → mcp_mesh-0.5.0.dist-info}/RECORD +55 -37
- mesh/__init__.py +6 -2
- mesh/decorators.py +143 -1
- /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/__init__.py +0 -0
- /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/dependency_resolution.py +0 -0
- /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/fast_heartbeat_check.py +0 -0
- /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/heartbeat_orchestrator.py +0 -0
- /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/heartbeat_pipeline.py +0 -0
- /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/heartbeat_send.py +0 -0
- /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/lifespan_integration.py +0 -0
- /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/registry_connection.py +0 -0
- /_mcp_mesh/pipeline/{startup → mcp_startup}/__init__.py +0 -0
- /_mcp_mesh/pipeline/{startup → mcp_startup}/configuration.py +0 -0
- /_mcp_mesh/pipeline/{startup → mcp_startup}/decorator_collection.py +0 -0
- /_mcp_mesh/pipeline/{startup → mcp_startup}/fastapiserver_setup.py +0 -0
- /_mcp_mesh/pipeline/{startup → mcp_startup}/heartbeat_preparation.py +0 -0
- /_mcp_mesh/pipeline/{startup → mcp_startup}/startup_pipeline.py +0 -0
- {mcp_mesh-0.4.1.dist-info → mcp_mesh-0.5.0.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.4.1.dist-info → mcp_mesh-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI lifespan integration for API heartbeat pipeline.
|
|
3
|
+
|
|
4
|
+
Handles the execution of API heartbeat pipeline as a background task
|
|
5
|
+
during FastAPI application lifespan for @mesh.route decorator services.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def api_heartbeat_lifespan_task(heartbeat_config: dict[str, Any]) -> None:
|
|
16
|
+
"""
|
|
17
|
+
API heartbeat task that runs in FastAPI lifespan using pipeline architecture.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
heartbeat_config: Configuration containing service_id, interval,
|
|
21
|
+
and context for API heartbeat execution
|
|
22
|
+
"""
|
|
23
|
+
service_id = heartbeat_config["service_id"]
|
|
24
|
+
interval = heartbeat_config["interval"] # Already validated by get_config_value in setup
|
|
25
|
+
context = heartbeat_config["context"]
|
|
26
|
+
standalone_mode = heartbeat_config.get("standalone_mode", False)
|
|
27
|
+
|
|
28
|
+
# Check if running in standalone mode
|
|
29
|
+
if standalone_mode:
|
|
30
|
+
logger.info(
|
|
31
|
+
f"💓 Starting API heartbeat pipeline in standalone mode for service '{service_id}' "
|
|
32
|
+
f"(no registry communication)"
|
|
33
|
+
)
|
|
34
|
+
return # For now, skip heartbeat in standalone mode
|
|
35
|
+
|
|
36
|
+
# Create API heartbeat orchestrator for pipeline execution
|
|
37
|
+
from .api_heartbeat_orchestrator import APIHeartbeatOrchestrator
|
|
38
|
+
|
|
39
|
+
api_heartbeat_orchestrator = APIHeartbeatOrchestrator()
|
|
40
|
+
|
|
41
|
+
logger.info(f"💓 Starting API heartbeat pipeline task for service '{service_id}'")
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
while True:
|
|
45
|
+
try:
|
|
46
|
+
# Execute API heartbeat pipeline
|
|
47
|
+
success = await api_heartbeat_orchestrator.execute_api_heartbeat(
|
|
48
|
+
service_id, context
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if not success:
|
|
52
|
+
# Log failure but continue to next cycle (pipeline handles detailed logging)
|
|
53
|
+
logger.debug(
|
|
54
|
+
f"💔 API heartbeat pipeline failed for service '{service_id}' - "
|
|
55
|
+
f"continuing to next cycle"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
except Exception as e:
|
|
59
|
+
# Log pipeline execution error but continue to next cycle for resilience
|
|
60
|
+
logger.error(
|
|
61
|
+
f"❌ API heartbeat pipeline execution error for service '{service_id}': {e}"
|
|
62
|
+
)
|
|
63
|
+
# Continue to next cycle - heartbeat should be resilient
|
|
64
|
+
|
|
65
|
+
# Wait for next heartbeat interval
|
|
66
|
+
await asyncio.sleep(interval)
|
|
67
|
+
|
|
68
|
+
except asyncio.CancelledError:
|
|
69
|
+
logger.info(f"🛑 API heartbeat pipeline task cancelled for service '{service_id}'")
|
|
70
|
+
raise
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def create_api_lifespan_handler(heartbeat_config: dict[str, Any]) -> Any:
|
|
74
|
+
"""
|
|
75
|
+
Create a FastAPI lifespan context manager that runs API heartbeat pipeline.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
heartbeat_config: Configuration for API heartbeat execution
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Async context manager for FastAPI lifespan
|
|
82
|
+
"""
|
|
83
|
+
from contextlib import asynccontextmanager
|
|
84
|
+
|
|
85
|
+
@asynccontextmanager
|
|
86
|
+
async def api_lifespan(app):
|
|
87
|
+
"""FastAPI lifespan context manager with API heartbeat integration."""
|
|
88
|
+
service_id = heartbeat_config.get("service_id", "unknown")
|
|
89
|
+
logger.info(f"🚀 Starting FastAPI lifespan for service '{service_id}'")
|
|
90
|
+
|
|
91
|
+
# Start API heartbeat task
|
|
92
|
+
heartbeat_task = asyncio.create_task(
|
|
93
|
+
api_heartbeat_lifespan_task(heartbeat_config)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# Yield control to FastAPI
|
|
98
|
+
yield
|
|
99
|
+
finally:
|
|
100
|
+
# Cleanup: cancel heartbeat task
|
|
101
|
+
logger.info(f"🛑 Shutting down FastAPI lifespan for service '{service_id}'")
|
|
102
|
+
heartbeat_task.cancel()
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
await heartbeat_task
|
|
106
|
+
except asyncio.CancelledError:
|
|
107
|
+
logger.info(f"✅ API heartbeat task cancelled for service '{service_id}'")
|
|
108
|
+
|
|
109
|
+
return api_lifespan
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def integrate_api_heartbeat_with_fastapi(
|
|
113
|
+
fastapi_app: Any, heartbeat_config: dict[str, Any]
|
|
114
|
+
) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Integrate API heartbeat pipeline with FastAPI lifespan events.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
fastapi_app: FastAPI application instance
|
|
120
|
+
heartbeat_config: Configuration for heartbeat execution
|
|
121
|
+
"""
|
|
122
|
+
service_id = heartbeat_config.get("service_id", "unknown")
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
# Check if FastAPI app already has a lifespan handler
|
|
126
|
+
existing_lifespan = getattr(fastapi_app, "router.lifespan_context", None)
|
|
127
|
+
|
|
128
|
+
if existing_lifespan is not None:
|
|
129
|
+
logger.warning(
|
|
130
|
+
f"⚠️ FastAPI app already has lifespan handler - "
|
|
131
|
+
f"API heartbeat integration may conflict for service '{service_id}'"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Create and set the lifespan handler
|
|
135
|
+
api_lifespan = create_api_lifespan_handler(heartbeat_config)
|
|
136
|
+
fastapi_app.router.lifespan_context = api_lifespan
|
|
137
|
+
|
|
138
|
+
logger.info(
|
|
139
|
+
f"🔗 API heartbeat integrated with FastAPI lifespan for service '{service_id}'"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.error(
|
|
144
|
+
f"❌ Failed to integrate API heartbeat with FastAPI lifespan "
|
|
145
|
+
f"for service '{service_id}': {e}"
|
|
146
|
+
)
|
|
147
|
+
raise
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API registry connection step for API heartbeat pipeline.
|
|
3
|
+
|
|
4
|
+
Prepares registry communication for FastAPI service heartbeat operations.
|
|
5
|
+
Simpler than MCP registry connection since API services don't require
|
|
6
|
+
complex dependency resolution.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..shared.base_step import PipelineStep
|
|
13
|
+
from ..shared.pipeline_types import PipelineResult
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class APIRegistryConnectionStep(PipelineStep):
|
|
19
|
+
"""
|
|
20
|
+
Prepare registry connection for API service heartbeat.
|
|
21
|
+
|
|
22
|
+
Ensures registry client is available and properly configured for
|
|
23
|
+
FastAPI service registration and health monitoring.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, required: bool = True):
|
|
27
|
+
super().__init__(
|
|
28
|
+
name="api-registry-connection",
|
|
29
|
+
required=required,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
async def execute(self, context: dict[str, Any]) -> PipelineResult:
|
|
33
|
+
"""
|
|
34
|
+
Prepare registry connection for API heartbeat operations.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
context: Pipeline context containing agent_config and registration_data
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
PipelineResult with registry_wrapper in context
|
|
41
|
+
"""
|
|
42
|
+
self.logger.info("🔗 [DEBUG] Preparing API registry connection for heartbeat")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# Check if registry_wrapper already exists in context
|
|
46
|
+
registry_wrapper = context.get("registry_wrapper")
|
|
47
|
+
|
|
48
|
+
if registry_wrapper is not None:
|
|
49
|
+
self.logger.debug("✅ Registry wrapper already available in context")
|
|
50
|
+
return PipelineResult(
|
|
51
|
+
message="Registry connection already established",
|
|
52
|
+
context={"registry_wrapper": registry_wrapper}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Get registry configuration from context
|
|
56
|
+
agent_config = context.get("agent_config", {})
|
|
57
|
+
registration_data = context.get("registration_data", {})
|
|
58
|
+
|
|
59
|
+
registry_url = (
|
|
60
|
+
agent_config.get("registry_url")
|
|
61
|
+
or registration_data.get("registry_url")
|
|
62
|
+
or "http://localhost:8000" # Default fallback
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
self.logger.debug(f"🔍 Using registry URL: {registry_url}")
|
|
66
|
+
|
|
67
|
+
# Create registry client wrapper
|
|
68
|
+
from ...generated.mcp_mesh_registry_client.api_client import ApiClient
|
|
69
|
+
from ...generated.mcp_mesh_registry_client.configuration import Configuration
|
|
70
|
+
from ...shared.registry_client_wrapper import RegistryClientWrapper
|
|
71
|
+
|
|
72
|
+
config = Configuration(host=registry_url)
|
|
73
|
+
api_client = ApiClient(configuration=config)
|
|
74
|
+
registry_wrapper = RegistryClientWrapper(api_client)
|
|
75
|
+
|
|
76
|
+
self.logger.info(f"🔗 API registry connection prepared: {registry_url}")
|
|
77
|
+
|
|
78
|
+
return PipelineResult(
|
|
79
|
+
message=f"Registry connection prepared for {registry_url}",
|
|
80
|
+
context={
|
|
81
|
+
"registry_wrapper": registry_wrapper,
|
|
82
|
+
"registry_url": registry_url,
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
error_msg = f"Failed to prepare API registry connection: {e}"
|
|
88
|
+
self.logger.error(f"❌ {error_msg}")
|
|
89
|
+
|
|
90
|
+
from ..shared.pipeline_types import PipelineStatus
|
|
91
|
+
result = PipelineResult(
|
|
92
|
+
status=PipelineStatus.FAILED,
|
|
93
|
+
message=error_msg,
|
|
94
|
+
context=context
|
|
95
|
+
)
|
|
96
|
+
result.add_error(str(e))
|
|
97
|
+
return result
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API Startup pipeline components for MCP Mesh.
|
|
3
|
+
|
|
4
|
+
Handles @mesh.route decorator collection, FastAPI app discovery,
|
|
5
|
+
and dependency injection setup during API service initialization.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .api_pipeline import APIPipeline
|
|
9
|
+
from .api_server_setup import APIServerSetupStep
|
|
10
|
+
from .fastapi_discovery import FastAPIAppDiscoveryStep
|
|
11
|
+
from .route_collection import RouteCollectionStep
|
|
12
|
+
from .route_integration import RouteIntegrationStep
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"RouteCollectionStep",
|
|
16
|
+
"FastAPIAppDiscoveryStep",
|
|
17
|
+
"RouteIntegrationStep",
|
|
18
|
+
"APIServerSetupStep",
|
|
19
|
+
"APIPipeline",
|
|
20
|
+
]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API pipeline for MCP Mesh FastAPI integration.
|
|
3
|
+
|
|
4
|
+
Provides structured execution of API operations with proper error handling
|
|
5
|
+
and logging. Handles @mesh.route decorator collection, FastAPI app discovery,
|
|
6
|
+
route integration, and service registration.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from ..shared.mesh_pipeline import MeshPipeline
|
|
12
|
+
from .api_server_setup import APIServerSetupStep
|
|
13
|
+
from .fastapi_discovery import FastAPIAppDiscoveryStep
|
|
14
|
+
from .route_collection import RouteCollectionStep
|
|
15
|
+
from .route_integration import RouteIntegrationStep
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class APIPipeline(MeshPipeline):
|
|
21
|
+
"""
|
|
22
|
+
Specialized pipeline for API operations.
|
|
23
|
+
|
|
24
|
+
Executes the core API integration steps in sequence:
|
|
25
|
+
1. Route collection (@mesh.route decorators)
|
|
26
|
+
2. FastAPI app discovery (find user's FastAPI instances)
|
|
27
|
+
3. Route integration (apply dependency injection)
|
|
28
|
+
4. API server setup (service registration metadata)
|
|
29
|
+
|
|
30
|
+
Unlike MCP agents, API services are consumers so we focus on:
|
|
31
|
+
- Dependency injection into route handlers
|
|
32
|
+
- Service registration for health monitoring
|
|
33
|
+
- NO server creation or binding (user owns FastAPI app)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, name: str = "api-pipeline"):
|
|
37
|
+
super().__init__(name=name)
|
|
38
|
+
self._setup_api_steps()
|
|
39
|
+
|
|
40
|
+
def _setup_api_steps(self) -> None:
|
|
41
|
+
"""Setup the API pipeline steps."""
|
|
42
|
+
# Essential API integration steps
|
|
43
|
+
steps = [
|
|
44
|
+
RouteCollectionStep(), # Collect @mesh.route decorators
|
|
45
|
+
FastAPIAppDiscoveryStep(), # Find user's FastAPI app instances
|
|
46
|
+
RouteIntegrationStep(), # Apply dependency injection to routes
|
|
47
|
+
APIServerSetupStep(), # Prepare service registration metadata
|
|
48
|
+
# Note: Heartbeat integration will be added in next phase
|
|
49
|
+
# Note: User controls FastAPI server startup (uvicorn/gunicorn)
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
self.add_steps(steps)
|
|
53
|
+
self.logger.debug(f"API pipeline configured with {len(steps)} steps")
|
|
54
|
+
|
|
55
|
+
# Log the pipeline strategy
|
|
56
|
+
self.logger.info(
|
|
57
|
+
f"🌐 [DEBUG] API Pipeline initialized: dependency injection for @mesh.route decorators"
|
|
58
|
+
)
|
|
59
|
+
self.logger.debug(
|
|
60
|
+
f"📋 Pipeline steps: {[step.name for step in steps]}"
|
|
61
|
+
)
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from ...shared.config_resolver import ValidationRule, get_config_value
|
|
6
|
+
from ...shared.defaults import MeshDefaults
|
|
7
|
+
from ...shared.host_resolver import HostResolver
|
|
8
|
+
from ..shared import PipelineResult, PipelineStatus, PipelineStep
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class APIServerSetupStep(PipelineStep):
|
|
12
|
+
"""
|
|
13
|
+
Minimal API server setup for FastAPI integration.
|
|
14
|
+
|
|
15
|
+
This step prepares the binding configuration and service registration
|
|
16
|
+
metadata for the user's existing FastAPI application. It does NOT create
|
|
17
|
+
or modify the FastAPI app - it only prepares the configuration needed
|
|
18
|
+
to run the app with uvicorn and register it with the mesh registry.
|
|
19
|
+
|
|
20
|
+
Our job is ONLY dependency injection - the user owns their FastAPI app.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
super().__init__(
|
|
25
|
+
name="api-server-setup",
|
|
26
|
+
required=True,
|
|
27
|
+
description="Prepare binding config and service registration for existing FastAPI app",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
async def execute(self, context: dict[str, Any]) -> PipelineResult:
|
|
31
|
+
"""Setup API server configuration."""
|
|
32
|
+
self.logger.debug("Setting up API server configuration...")
|
|
33
|
+
|
|
34
|
+
result = PipelineResult(message="API server setup completed")
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
# Verify we have FastAPI apps to work with
|
|
38
|
+
fastapi_apps = context.get("fastapi_apps", {})
|
|
39
|
+
integration_results = context.get("integration_results", {})
|
|
40
|
+
|
|
41
|
+
if not fastapi_apps:
|
|
42
|
+
result.status = PipelineStatus.FAILED
|
|
43
|
+
result.message = "No FastAPI applications found"
|
|
44
|
+
result.add_error("Cannot setup API server without existing FastAPI app")
|
|
45
|
+
self.logger.error(
|
|
46
|
+
"❌ No FastAPI applications found. API pipeline requires "
|
|
47
|
+
"an existing FastAPI app with @mesh.route decorators."
|
|
48
|
+
)
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
# For now, we only support single FastAPI app
|
|
52
|
+
# TODO: Future enhancement could support multiple apps
|
|
53
|
+
if len(fastapi_apps) > 1:
|
|
54
|
+
self.logger.warning(
|
|
55
|
+
f"⚠️ Multiple FastAPI apps found ({len(fastapi_apps)}), "
|
|
56
|
+
f"using the first one. Multi-app support coming in future."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Get the primary FastAPI app
|
|
60
|
+
primary_app_id = list(fastapi_apps.keys())[0]
|
|
61
|
+
primary_app_info = fastapi_apps[primary_app_id]
|
|
62
|
+
primary_app = primary_app_info["instance"]
|
|
63
|
+
|
|
64
|
+
self.logger.info(
|
|
65
|
+
f"🎯 Using FastAPI app: '{primary_app_info['title']}' as primary app"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Prepare display configuration for registry (NOT binding configuration)
|
|
69
|
+
display_config = self._prepare_display_config()
|
|
70
|
+
|
|
71
|
+
# Prepare service registration metadata
|
|
72
|
+
service_metadata = self._prepare_service_metadata(
|
|
73
|
+
primary_app_info, integration_results.get(primary_app_id, {}), display_config
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Prepare heartbeat configuration
|
|
77
|
+
heartbeat_config = self._prepare_heartbeat_config(
|
|
78
|
+
primary_app_info, display_config, service_metadata
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Store configuration in context
|
|
82
|
+
result.add_context("primary_fastapi_app", primary_app)
|
|
83
|
+
result.add_context("fastapi_app", primary_app) # For heartbeat compatibility
|
|
84
|
+
result.add_context("api_display_config", display_config)
|
|
85
|
+
result.add_context("display_config", display_config) # For heartbeat compatibility
|
|
86
|
+
result.add_context("api_service_metadata", service_metadata)
|
|
87
|
+
result.add_context("service_type", "api") # Important for registry registration
|
|
88
|
+
result.add_context("heartbeat_config", heartbeat_config)
|
|
89
|
+
|
|
90
|
+
# Update result message
|
|
91
|
+
integrated_routes = integration_results.get(primary_app_id, {}).get("integrated_count", 0)
|
|
92
|
+
result.message = (
|
|
93
|
+
f"API server config prepared for '{primary_app_info['title']}' "
|
|
94
|
+
f"with {integrated_routes} dependency-injected routes"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
self.logger.info(
|
|
98
|
+
f"✅ API server setup: {primary_app_info['title']} ready "
|
|
99
|
+
f"(registry display: {display_config['display_host']}:{display_config['display_port']})"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
result.status = PipelineStatus.FAILED
|
|
104
|
+
result.message = f"API server setup failed: {e}"
|
|
105
|
+
result.add_error(str(e))
|
|
106
|
+
self.logger.error(f"❌ API server setup failed: {e}")
|
|
107
|
+
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
def _prepare_display_config(self) -> Dict[str, Any]:
|
|
111
|
+
"""
|
|
112
|
+
Prepare display configuration for service registration.
|
|
113
|
+
|
|
114
|
+
This is ONLY for registry display purposes since FastAPI services
|
|
115
|
+
are consumers (nobody needs to communicate TO them in mesh network).
|
|
116
|
+
Users configure their actual uvicorn host/port separately.
|
|
117
|
+
"""
|
|
118
|
+
# Get external host for display (what others would see this service as)
|
|
119
|
+
external_host = HostResolver.get_external_host()
|
|
120
|
+
|
|
121
|
+
# Get port for display - users can override via env var
|
|
122
|
+
display_port = get_config_value(
|
|
123
|
+
"MCP_MESH_HTTP_PORT",
|
|
124
|
+
default=8080, # FastAPI convention
|
|
125
|
+
rule=ValidationRule.PORT_RULE,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Also check if user provided host override
|
|
129
|
+
display_host = get_config_value(
|
|
130
|
+
"MCP_MESH_HTTP_HOST",
|
|
131
|
+
default=external_host,
|
|
132
|
+
rule=ValidationRule.STRING_RULE,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
display_config = {
|
|
136
|
+
"display_host": display_host,
|
|
137
|
+
"display_port": display_port,
|
|
138
|
+
"external_host": external_host,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
self.logger.debug(
|
|
142
|
+
f"📍 Display config: {display_host}:{display_port} "
|
|
143
|
+
f"(for registry display only - user controls actual uvicorn binding)"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return display_config
|
|
147
|
+
|
|
148
|
+
def _prepare_service_metadata(
|
|
149
|
+
self, app_info: Dict[str, Any], integration_results: Dict[str, Any], display_config: Dict[str, Any]
|
|
150
|
+
) -> Dict[str, Any]:
|
|
151
|
+
"""
|
|
152
|
+
Prepare service registration metadata for mesh registry.
|
|
153
|
+
|
|
154
|
+
This metadata tells the mesh registry about our API service
|
|
155
|
+
and what capabilities it provides (routes, not MCP tools).
|
|
156
|
+
"""
|
|
157
|
+
# Extract route information for registry
|
|
158
|
+
route_capabilities = []
|
|
159
|
+
route_details = integration_results.get("route_details", {})
|
|
160
|
+
|
|
161
|
+
for route_name, details in route_details.items():
|
|
162
|
+
if details.get("status") == "integrated":
|
|
163
|
+
# Create capability entry for each dependency-injected route
|
|
164
|
+
capability_info = {
|
|
165
|
+
"name": route_name,
|
|
166
|
+
"type": "api_route",
|
|
167
|
+
"dependencies": details.get("dependencies", []),
|
|
168
|
+
"dependency_count": details.get("dependency_count", 0),
|
|
169
|
+
}
|
|
170
|
+
route_capabilities.append(capability_info)
|
|
171
|
+
|
|
172
|
+
# Build service metadata
|
|
173
|
+
service_metadata = {
|
|
174
|
+
"service_name": app_info.get("title", "FastAPI Service"),
|
|
175
|
+
"service_version": app_info.get("version", "1.0.0"),
|
|
176
|
+
"service_type": "api", # Distinguishes from MCP agents
|
|
177
|
+
"capabilities": route_capabilities,
|
|
178
|
+
"total_routes": len(app_info.get("routes", [])),
|
|
179
|
+
"integrated_routes": integration_results.get("integrated_count", 0),
|
|
180
|
+
"framework": "fastapi",
|
|
181
|
+
"integration_method": "mesh_route_decorators",
|
|
182
|
+
# Display info for registry (NOT actual binding)
|
|
183
|
+
"display_host": display_config["display_host"],
|
|
184
|
+
"display_port": display_config["display_port"],
|
|
185
|
+
"external_host": display_config["external_host"],
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
self.logger.debug(
|
|
189
|
+
f"📋 Service metadata: {service_metadata['service_name']} "
|
|
190
|
+
f"({len(route_capabilities)} capabilities)"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return service_metadata
|
|
194
|
+
|
|
195
|
+
def _prepare_heartbeat_config(
|
|
196
|
+
self, app_info: Dict[str, Any], display_config: Dict[str, Any], service_metadata: Dict[str, Any]
|
|
197
|
+
) -> Dict[str, Any]:
|
|
198
|
+
"""
|
|
199
|
+
Prepare heartbeat configuration for API service.
|
|
200
|
+
|
|
201
|
+
This configuration will be used to start the heartbeat pipeline
|
|
202
|
+
for periodic service registration and health monitoring.
|
|
203
|
+
"""
|
|
204
|
+
# Generate service ID using same logic as MCP agents
|
|
205
|
+
service_id = self._generate_api_service_id(app_info)
|
|
206
|
+
|
|
207
|
+
# Get heartbeat interval using centralized defaults (consistent with MCP heartbeat)
|
|
208
|
+
from ...shared.defaults import MeshDefaults
|
|
209
|
+
|
|
210
|
+
heartbeat_interval = get_config_value(
|
|
211
|
+
"MCP_MESH_HEALTH_INTERVAL",
|
|
212
|
+
default=MeshDefaults.HEALTH_INTERVAL,
|
|
213
|
+
rule=ValidationRule.NONZERO_RULE,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Check if standalone mode (no registry communication)
|
|
217
|
+
standalone_mode = get_config_value(
|
|
218
|
+
"MCP_MESH_STANDALONE",
|
|
219
|
+
default=False,
|
|
220
|
+
rule=ValidationRule.TRUTHY_RULE,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
heartbeat_config = {
|
|
224
|
+
"service_id": service_id,
|
|
225
|
+
"interval": heartbeat_interval,
|
|
226
|
+
"standalone_mode": standalone_mode,
|
|
227
|
+
"context": {
|
|
228
|
+
# Context will be populated during heartbeat execution
|
|
229
|
+
# with current pipeline context including fastapi_app, display_config, etc.
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
self.logger.info(
|
|
234
|
+
f"💓 API heartbeat config created: service_id='{service_id}', "
|
|
235
|
+
f"interval={heartbeat_interval}s, standalone={standalone_mode}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return heartbeat_config
|
|
239
|
+
|
|
240
|
+
def _generate_api_service_id(self, app_info: Dict[str, Any]) -> str:
|
|
241
|
+
"""
|
|
242
|
+
Generate API service ID using same UUID logic as MCP agents.
|
|
243
|
+
|
|
244
|
+
Logic:
|
|
245
|
+
- If no name provided: "api-{uuid8}"
|
|
246
|
+
- If name doesn't contain "api": "{name}-api-{uuid8}"
|
|
247
|
+
- If name contains "api": "{name}-{uuid8}"
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
app_info: FastAPI app information containing title
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Generated service ID with UUID suffix
|
|
254
|
+
"""
|
|
255
|
+
# Get service name from app title or use default
|
|
256
|
+
service_name = app_info.get("title", "")
|
|
257
|
+
|
|
258
|
+
# Clean the service name (similar to MCP agent logic)
|
|
259
|
+
if service_name:
|
|
260
|
+
# Convert to lowercase and replace spaces/special chars with hyphens
|
|
261
|
+
cleaned_name = service_name.lower().replace(" ", "-").replace("_", "-")
|
|
262
|
+
# Remove any double hyphens and strip
|
|
263
|
+
cleaned_name = "-".join(part for part in cleaned_name.split("-") if part)
|
|
264
|
+
else:
|
|
265
|
+
cleaned_name = ""
|
|
266
|
+
|
|
267
|
+
# Apply the UUID suffix logic similar to MCP agents
|
|
268
|
+
uuid_suffix = str(uuid.uuid4())[:8]
|
|
269
|
+
|
|
270
|
+
if not cleaned_name:
|
|
271
|
+
# No name provided: use "api-{uuid8}"
|
|
272
|
+
service_id = f"api-{uuid_suffix}"
|
|
273
|
+
elif "api" in cleaned_name.lower():
|
|
274
|
+
# Name contains "api": use "{name}-{uuid8}"
|
|
275
|
+
service_id = f"{cleaned_name}-{uuid_suffix}"
|
|
276
|
+
else:
|
|
277
|
+
# Name doesn't contain "api": use "{name}-api-{uuid8}"
|
|
278
|
+
service_id = f"{cleaned_name}-api-{uuid_suffix}"
|
|
279
|
+
|
|
280
|
+
self.logger.debug(
|
|
281
|
+
f"Generated API service ID: '{service_id}' from app title: '{service_name}'"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return service_id
|
|
285
|
+
|
|
286
|
+
def _is_http_enabled(self) -> bool:
|
|
287
|
+
"""Check if HTTP transport is enabled."""
|
|
288
|
+
return get_config_value(
|
|
289
|
+
"MCP_MESH_HTTP_ENABLED",
|
|
290
|
+
default=True,
|
|
291
|
+
rule=ValidationRule.TRUTHY_RULE,
|
|
292
|
+
)
|