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.
Files changed (55) hide show
  1. _mcp_mesh/__init__.py +14 -3
  2. _mcp_mesh/engine/async_mcp_client.py +6 -19
  3. _mcp_mesh/engine/dependency_injector.py +161 -74
  4. _mcp_mesh/engine/full_mcp_proxy.py +25 -20
  5. _mcp_mesh/engine/mcp_client_proxy.py +5 -19
  6. _mcp_mesh/generated/.openapi-generator/FILES +2 -0
  7. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +2 -0
  8. _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +1 -0
  9. _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +305 -0
  10. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +1 -0
  11. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +10 -1
  12. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +4 -4
  13. _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +108 -0
  14. _mcp_mesh/pipeline/__init__.py +2 -2
  15. _mcp_mesh/pipeline/api_heartbeat/__init__.py +16 -0
  16. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +515 -0
  17. _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +117 -0
  18. _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +140 -0
  19. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +247 -0
  20. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +309 -0
  21. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +332 -0
  22. _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +147 -0
  23. _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +97 -0
  24. _mcp_mesh/pipeline/api_startup/__init__.py +20 -0
  25. _mcp_mesh/pipeline/api_startup/api_pipeline.py +61 -0
  26. _mcp_mesh/pipeline/api_startup/api_server_setup.py +292 -0
  27. _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +302 -0
  28. _mcp_mesh/pipeline/api_startup/route_collection.py +56 -0
  29. _mcp_mesh/pipeline/api_startup/route_integration.py +318 -0
  30. _mcp_mesh/pipeline/{startup → mcp_startup}/fastmcpserver_discovery.py +4 -4
  31. _mcp_mesh/pipeline/{startup → mcp_startup}/heartbeat_loop.py +1 -1
  32. _mcp_mesh/pipeline/{startup → mcp_startup}/startup_orchestrator.py +170 -5
  33. _mcp_mesh/shared/config_resolver.py +0 -3
  34. _mcp_mesh/shared/logging_config.py +2 -1
  35. _mcp_mesh/shared/sse_parser.py +217 -0
  36. {mcp_mesh-0.4.1.dist-info → mcp_mesh-0.5.0.dist-info}/METADATA +1 -1
  37. {mcp_mesh-0.4.1.dist-info → mcp_mesh-0.5.0.dist-info}/RECORD +55 -37
  38. mesh/__init__.py +6 -2
  39. mesh/decorators.py +143 -1
  40. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/__init__.py +0 -0
  41. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/dependency_resolution.py +0 -0
  42. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/fast_heartbeat_check.py +0 -0
  43. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/heartbeat_orchestrator.py +0 -0
  44. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/heartbeat_pipeline.py +0 -0
  45. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/heartbeat_send.py +0 -0
  46. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/lifespan_integration.py +0 -0
  47. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/registry_connection.py +0 -0
  48. /_mcp_mesh/pipeline/{startup → mcp_startup}/__init__.py +0 -0
  49. /_mcp_mesh/pipeline/{startup → mcp_startup}/configuration.py +0 -0
  50. /_mcp_mesh/pipeline/{startup → mcp_startup}/decorator_collection.py +0 -0
  51. /_mcp_mesh/pipeline/{startup → mcp_startup}/fastapiserver_setup.py +0 -0
  52. /_mcp_mesh/pipeline/{startup → mcp_startup}/heartbeat_preparation.py +0 -0
  53. /_mcp_mesh/pipeline/{startup → mcp_startup}/startup_pipeline.py +0 -0
  54. {mcp_mesh-0.4.1.dist-info → mcp_mesh-0.5.0.dist-info}/WHEEL +0 -0
  55. {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
+ )