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,302 @@
1
+ import gc
2
+ import logging
3
+ from typing import Any, Dict, List, Optional, Tuple
4
+
5
+ from ..shared import PipelineResult, PipelineStatus, PipelineStep
6
+
7
+
8
+ class FastAPIAppDiscoveryStep(PipelineStep):
9
+ """
10
+ Discovers existing FastAPI application instances in the user's code.
11
+
12
+ This step scans the Python runtime to find FastAPI applications that
13
+ have been instantiated by the user, without modifying them in any way.
14
+
15
+ The goal is minimal intervention - we only discover what exists,
16
+ we don't create or modify anything.
17
+ """
18
+
19
+ def __init__(self):
20
+ super().__init__(
21
+ name="fastapi-discovery",
22
+ required=True,
23
+ description="Discover existing FastAPI application instances",
24
+ )
25
+
26
+ async def execute(self, context: dict[str, Any]) -> PipelineResult:
27
+ """Discover FastAPI applications."""
28
+ self.logger.debug("Discovering FastAPI applications...")
29
+
30
+ result = PipelineResult(message="FastAPI discovery completed")
31
+
32
+ try:
33
+ # Get route decorators from context (from RouteCollectionStep)
34
+ mesh_routes = context.get("mesh_routes", {})
35
+
36
+ if not mesh_routes:
37
+ result.status = PipelineStatus.SKIPPED
38
+ result.message = "No @mesh.route decorators found"
39
+ self.logger.info("⚠️ No @mesh.route decorators found to process")
40
+ return result
41
+
42
+ # Discover FastAPI instances
43
+ fastapi_apps = self._discover_fastapi_instances()
44
+
45
+ if not fastapi_apps:
46
+ # This is not necessarily an error - user might be using FastAPI differently
47
+ result.status = PipelineStatus.FAILED
48
+ result.message = "No FastAPI applications found"
49
+ result.add_error("No FastAPI applications discovered in runtime")
50
+ self.logger.error(
51
+ "❌ No FastAPI applications found. @mesh.route decorators require "
52
+ "an existing FastAPI app instance. Please create a FastAPI app before "
53
+ "using @mesh.route decorators."
54
+ )
55
+ return result
56
+
57
+ # Analyze which routes belong to which apps
58
+ route_mapping = self._map_routes_to_apps(fastapi_apps, mesh_routes)
59
+
60
+ # Store discovery results in context
61
+ result.add_context("fastapi_apps", fastapi_apps)
62
+ result.add_context("route_mapping", route_mapping)
63
+ result.add_context("discovered_app_count", len(fastapi_apps))
64
+
65
+ # Update result message
66
+ route_count = sum(len(routes) for routes in route_mapping.values())
67
+ result.message = (
68
+ f"Discovered {len(fastapi_apps)} FastAPI app(s) with "
69
+ f"{route_count} @mesh.route decorated handlers"
70
+ )
71
+
72
+ self.logger.info(
73
+ f"📦 FastAPI Discovery: {len(fastapi_apps)} app(s), "
74
+ f"{route_count} @mesh.route handlers, {len(mesh_routes)} total routes"
75
+ )
76
+
77
+ # Log details for debugging
78
+ for app_id, app_info in fastapi_apps.items():
79
+ app_title = app_info.get("title", "Unknown")
80
+ routes_in_app = len(route_mapping.get(app_id, {}))
81
+ self.logger.debug(
82
+ f" App '{app_title}' ({app_id}): {routes_in_app} @mesh.route handlers"
83
+ )
84
+
85
+ except Exception as e:
86
+ result.status = PipelineStatus.FAILED
87
+ result.message = f"FastAPI discovery failed: {e}"
88
+ result.add_error(str(e))
89
+ self.logger.error(f"❌ FastAPI discovery failed: {e}")
90
+
91
+ return result
92
+
93
+ def _discover_fastapi_instances(self) -> Dict[str, Dict[str, Any]]:
94
+ """
95
+ Discover FastAPI application instances in the Python runtime.
96
+
97
+ Uses intelligent deduplication to handle standard uvicorn patterns where
98
+ the same app might be imported multiple times (e.g., "module:app" pattern).
99
+
100
+ Returns:
101
+ Dict mapping app_id -> app_info where app_info contains:
102
+ - 'instance': The FastAPI app instance
103
+ - 'title': App title from FastAPI
104
+ - 'routes': List of route information
105
+ - 'module': Module where app was found
106
+ """
107
+ fastapi_apps = {}
108
+ seen_apps = {} # For deduplication: title -> app_info
109
+
110
+ try:
111
+ # Import FastAPI here to avoid dependency if not used
112
+ from fastapi import FastAPI
113
+ except ImportError:
114
+ self.logger.warning("FastAPI not installed - cannot discover FastAPI apps")
115
+ return {}
116
+
117
+ # Scan garbage collector for FastAPI instances
118
+ candidate_apps = []
119
+ for obj in gc.get_objects():
120
+ if isinstance(obj, FastAPI):
121
+ candidate_apps.append(obj)
122
+
123
+ # Deduplicate apps with identical configurations
124
+ for obj in candidate_apps:
125
+ try:
126
+ title = getattr(obj, "title", "FastAPI App")
127
+ version = getattr(obj, "version", "unknown")
128
+ routes = self._extract_route_info(obj)
129
+ route_count = len(routes)
130
+
131
+ # Create a signature for deduplication
132
+ app_signature = (title, version, route_count)
133
+
134
+ # Check if we've seen an identical app
135
+ if app_signature in seen_apps:
136
+ existing_app = seen_apps[app_signature]
137
+ # Compare route details to ensure they're truly identical
138
+ existing_routes = existing_app["routes"]
139
+
140
+ if self._routes_are_identical(routes, existing_routes):
141
+ self.logger.debug(
142
+ f"Skipping duplicate FastAPI app: '{title}' (same title, version, and routes)"
143
+ )
144
+ continue # Skip this duplicate
145
+
146
+ # This is a unique app, add it
147
+ app_id = f"app_{id(obj)}"
148
+ app_info = {
149
+ "instance": obj,
150
+ "title": title,
151
+ "version": version,
152
+ "routes": routes,
153
+ "module": self._get_app_module(obj),
154
+ "object_id": id(obj),
155
+ "router_routes_count": len(obj.router.routes) if hasattr(obj, 'router') else 0,
156
+ }
157
+
158
+ fastapi_apps[app_id] = app_info
159
+ seen_apps[app_signature] = app_info
160
+
161
+ self.logger.debug(
162
+ f"Found FastAPI app: '{title}' (module: {app_info['module']}) with "
163
+ f"{len(routes)} routes"
164
+ )
165
+
166
+ except Exception as e:
167
+ self.logger.warning(f"Error analyzing FastAPI app: {e}")
168
+ continue
169
+
170
+ return fastapi_apps
171
+
172
+ def _routes_are_identical(self, routes1: List[Dict[str, Any]], routes2: List[Dict[str, Any]]) -> bool:
173
+ """
174
+ Compare two route lists to see if they're identical.
175
+
176
+ Args:
177
+ routes1: First route list
178
+ routes2: Second route list
179
+
180
+ Returns:
181
+ True if routes are identical, False otherwise
182
+ """
183
+ if len(routes1) != len(routes2):
184
+ return False
185
+
186
+ # Create comparable signatures for each route
187
+ def route_signature(route):
188
+ return (
189
+ tuple(sorted(route.get('methods', []))), # Sort methods for consistent comparison
190
+ route.get('path', ''),
191
+ route.get('endpoint_name', '')
192
+ )
193
+
194
+ # Sort routes by signature for consistent comparison
195
+ sig1 = sorted([route_signature(r) for r in routes1])
196
+ sig2 = sorted([route_signature(r) for r in routes2])
197
+
198
+ return sig1 == sig2
199
+
200
+ def _extract_route_info(self, app) -> List[Dict[str, Any]]:
201
+ """
202
+ Extract route information from FastAPI app without modifying it.
203
+
204
+ Args:
205
+ app: FastAPI application instance
206
+
207
+ Returns:
208
+ List of route information dictionaries
209
+ """
210
+ routes = []
211
+
212
+ try:
213
+ for route in app.router.routes:
214
+ if hasattr(route, 'endpoint') and hasattr(route, 'path'):
215
+ route_info = {
216
+ "path": route.path,
217
+ "methods": list(route.methods) if hasattr(route, 'methods') else [],
218
+ "endpoint": route.endpoint,
219
+ "endpoint_name": getattr(route.endpoint, '__name__', 'unknown'),
220
+ "has_mesh_route": hasattr(route.endpoint, '_mesh_route_metadata'),
221
+ }
222
+ routes.append(route_info)
223
+
224
+ except Exception as e:
225
+ self.logger.warning(f"Error extracting route info: {e}")
226
+
227
+ return routes
228
+
229
+ def _get_app_module(self, app) -> Optional[str]:
230
+ """
231
+ Try to determine which module the FastAPI app belongs to.
232
+
233
+ Args:
234
+ app: FastAPI application instance
235
+
236
+ Returns:
237
+ Module name or None if unknown
238
+ """
239
+ try:
240
+ # Try to get module from the app's stack frame when it was created
241
+ # This is best-effort - may not always work
242
+ import inspect
243
+
244
+ frame = inspect.currentframe()
245
+ while frame:
246
+ frame_globals = frame.f_globals
247
+ for name, obj in frame_globals.items():
248
+ if obj is app:
249
+ return frame_globals.get('__name__', 'unknown')
250
+ frame = frame.f_back
251
+
252
+ except Exception:
253
+ pass
254
+
255
+ return None
256
+
257
+ def _map_routes_to_apps(
258
+ self,
259
+ fastapi_apps: Dict[str, Dict[str, Any]],
260
+ mesh_routes: Dict[str, Any]
261
+ ) -> Dict[str, Dict[str, Any]]:
262
+ """
263
+ Map @mesh.route decorated functions to their FastAPI applications.
264
+
265
+ Args:
266
+ fastapi_apps: Discovered FastAPI applications
267
+ mesh_routes: @mesh.route decorated functions from DecoratorRegistry
268
+
269
+ Returns:
270
+ Dict mapping app_id -> {route_name -> route_info} for routes that have @mesh.route
271
+ """
272
+ route_mapping = {}
273
+
274
+ for app_id, app_info in fastapi_apps.items():
275
+ app_routes = {}
276
+
277
+ for route_info in app_info["routes"]:
278
+ endpoint_name = route_info["endpoint_name"]
279
+
280
+ # Check if this route handler has @mesh.route decorator
281
+ if endpoint_name in mesh_routes:
282
+ mesh_route_data = mesh_routes[endpoint_name]
283
+
284
+ # Combine FastAPI route info with @mesh.route metadata
285
+ combined_info = {
286
+ **route_info, # FastAPI route info
287
+ "mesh_metadata": mesh_route_data.metadata, # @mesh.route metadata
288
+ "dependencies": mesh_route_data.metadata.get("dependencies", []),
289
+ "mesh_decorator": mesh_route_data, # Full DecoratedFunction object
290
+ }
291
+
292
+ app_routes[endpoint_name] = combined_info
293
+
294
+ self.logger.debug(
295
+ f"Mapped route '{endpoint_name}' to app '{app_info['title']}' "
296
+ f"with {len(combined_info['dependencies'])} dependencies"
297
+ )
298
+
299
+ if app_routes:
300
+ route_mapping[app_id] = app_routes
301
+
302
+ return route_mapping
@@ -0,0 +1,56 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ from ...engine.decorator_registry import DecoratorRegistry
5
+ from ..shared import PipelineResult, PipelineStatus, PipelineStep
6
+
7
+
8
+ class RouteCollectionStep(PipelineStep):
9
+ """
10
+ Collects all registered @mesh.route decorators from DecoratorRegistry.
11
+
12
+ This step reads the current state of route decorator registrations and
13
+ makes them available for subsequent processing steps.
14
+ """
15
+
16
+ def __init__(self):
17
+ super().__init__(
18
+ name="route-collection",
19
+ required=True,
20
+ description="Collect all registered @mesh.route decorators",
21
+ )
22
+
23
+ async def execute(self, context: dict[str, Any]) -> PipelineResult:
24
+ """Collect route decorators from registry."""
25
+ self.logger.debug("Collecting route decorators from DecoratorRegistry...")
26
+
27
+ result = PipelineResult(message="Route collection completed")
28
+
29
+ try:
30
+ # Get all registered route decorators
31
+ mesh_routes = DecoratorRegistry.get_all_by_type("mesh_route")
32
+
33
+ # Store in context for subsequent steps
34
+ result.add_context("mesh_routes", mesh_routes)
35
+ result.add_context("route_count", len(mesh_routes))
36
+
37
+ # Update result message
38
+ result.message = f"Collected {len(mesh_routes)} routes"
39
+
40
+ self.logger.info(
41
+ f"📦 Collected decorators: {len(mesh_routes)} @mesh.route"
42
+ )
43
+
44
+ # Validate we have routes to process
45
+ if len(mesh_routes) == 0:
46
+ result.status = PipelineStatus.SKIPPED
47
+ result.message = "No route decorators found to process"
48
+ self.logger.warning("⚠️ No route decorators found in registry")
49
+
50
+ except Exception as e:
51
+ result.status = PipelineStatus.FAILED
52
+ result.message = f"Failed to collect route decorators: {e}"
53
+ result.add_error(str(e))
54
+ self.logger.error(f"❌ Route collection failed: {e}")
55
+
56
+ return result
@@ -0,0 +1,318 @@
1
+ import logging
2
+ from typing import Any, Dict
3
+
4
+ from ...engine.dependency_injector import get_global_injector
5
+ from ..shared import PipelineResult, PipelineStatus, PipelineStep
6
+
7
+
8
+ class RouteIntegrationStep(PipelineStep):
9
+ """
10
+ Integrates dependency injection into FastAPI route handlers.
11
+
12
+ This step takes the discovered FastAPI apps and @mesh.route decorated handlers,
13
+ then applies dependency injection by replacing the route.endpoint with a
14
+ dependency injection wrapper.
15
+
16
+ Uses the existing dependency injection engine from MCP tools - route handlers
17
+ are just functions, so the same injection logic applies perfectly.
18
+ """
19
+
20
+ def __init__(self):
21
+ super().__init__(
22
+ name="route-integration",
23
+ required=True,
24
+ description="Apply dependency injection to @mesh.route decorated handlers",
25
+ )
26
+
27
+ async def execute(self, context: dict[str, Any]) -> PipelineResult:
28
+ """Apply dependency injection to route handlers."""
29
+ self.logger.debug("Applying dependency injection to route handlers...")
30
+
31
+ result = PipelineResult(message="Route integration completed")
32
+
33
+ try:
34
+ # Get discovery results from context
35
+ fastapi_apps = context.get("fastapi_apps", {})
36
+ route_mapping = context.get("route_mapping", {})
37
+
38
+ if not fastapi_apps:
39
+ result.status = PipelineStatus.SKIPPED
40
+ result.message = "No FastAPI applications found"
41
+ self.logger.warning("⚠️ No FastAPI applications to integrate")
42
+ return result
43
+
44
+ if not route_mapping:
45
+ result.status = PipelineStatus.SKIPPED
46
+ result.message = "No @mesh.route handlers found"
47
+ self.logger.warning("⚠️ No @mesh.route handlers to integrate")
48
+ return result
49
+
50
+ # Apply dependency injection to each app's routes
51
+ integration_results = {}
52
+ total_integrated = 0
53
+
54
+ for app_id, app_info in fastapi_apps.items():
55
+ if app_id not in route_mapping:
56
+ continue
57
+
58
+ app_results = self._integrate_app_routes(
59
+ app_info, route_mapping[app_id]
60
+ )
61
+ integration_results[app_id] = app_results
62
+ total_integrated += app_results["integrated_count"]
63
+
64
+ self.logger.info(
65
+ f"📝 Integrated {app_results['integrated_count']} routes in "
66
+ f"'{app_info['title']}'"
67
+ )
68
+
69
+ # Store integration results in context
70
+ result.add_context("integration_results", integration_results)
71
+ result.add_context("total_integrated_routes", total_integrated)
72
+
73
+ # Update result message
74
+ result.message = f"Integrated {total_integrated} route handlers with dependency injection"
75
+
76
+ self.logger.info(
77
+ f"✅ Route Integration: {total_integrated} handlers now have dependency injection"
78
+ )
79
+
80
+ except Exception as e:
81
+ result.status = PipelineStatus.FAILED
82
+ result.message = f"Route integration failed: {e}"
83
+ result.add_error(str(e))
84
+ self.logger.error(f"❌ Route integration failed: {e}")
85
+
86
+ return result
87
+
88
+ def _integrate_app_routes(
89
+ self, app_info: Dict[str, Any], route_mapping: Dict[str, Any]
90
+ ) -> Dict[str, Any]:
91
+ """
92
+ Apply dependency injection to routes in a single FastAPI app.
93
+
94
+ Args:
95
+ app_info: FastAPI app information from discovery
96
+ route_mapping: Route mapping for this specific app
97
+
98
+ Returns:
99
+ Integration results for this app
100
+ """
101
+ app = app_info["instance"]
102
+ app_title = app_info["title"]
103
+ injector = get_global_injector()
104
+
105
+ integration_results = {
106
+ "app_title": app_title,
107
+ "integrated_count": 0,
108
+ "skipped_count": 0,
109
+ "error_count": 0,
110
+ "route_details": {},
111
+ }
112
+
113
+ # Process each @mesh.route decorated handler
114
+ for route_name, route_info in route_mapping.items():
115
+ try:
116
+ result_detail = self._integrate_single_route(
117
+ app, route_info, injector
118
+ )
119
+ integration_results["route_details"][route_name] = result_detail
120
+
121
+ if result_detail["status"] == "integrated":
122
+ integration_results["integrated_count"] += 1
123
+ elif result_detail["status"] == "skipped":
124
+ integration_results["skipped_count"] += 1
125
+ else:
126
+ integration_results["error_count"] += 1
127
+
128
+ except Exception as e:
129
+ self.logger.error(
130
+ f"❌ Failed to integrate route '{route_name}': {e}"
131
+ )
132
+ integration_results["error_count"] += 1
133
+ integration_results["route_details"][route_name] = {
134
+ "status": "error",
135
+ "error": str(e)
136
+ }
137
+
138
+ return integration_results
139
+
140
+ def _integrate_single_route(
141
+ self, app, route_info: Dict[str, Any], injector
142
+ ) -> Dict[str, Any]:
143
+ """
144
+ Apply dependency injection to a single route handler.
145
+
146
+ Args:
147
+ app: FastAPI application instance
148
+ route_info: Route information including dependencies
149
+ injector: Dependency injector instance
150
+
151
+ Returns:
152
+ Integration result details
153
+ """
154
+ endpoint_name = route_info["endpoint_name"]
155
+ original_handler = route_info["endpoint"]
156
+ dependencies = route_info["dependencies"]
157
+ path = route_info["path"]
158
+ methods = route_info["methods"]
159
+
160
+ # Extract dependency names for injector
161
+ dependency_names = [dep["capability"] for dep in dependencies]
162
+
163
+ self.logger.debug(
164
+ f"🔧 Integrating route {methods} {path} -> {endpoint_name}() "
165
+ f"with dependencies: {dependency_names}"
166
+ )
167
+ self.logger.debug(f"🔍 Route integration processing: {original_handler} at {hex(id(original_handler))}")
168
+
169
+ # Skip if no dependencies
170
+ if not dependency_names:
171
+ self.logger.debug(f"⚠️ Route '{endpoint_name}' has no dependencies, skipping")
172
+ return {
173
+ "status": "skipped",
174
+ "reason": "no_dependencies",
175
+ "dependency_count": 0
176
+ }
177
+
178
+ # Check if function already has an injection wrapper (from @mesh.route decorator)
179
+ # The function might be the wrapper itself (if decorator order is correct)
180
+ is_already_wrapper = getattr(original_handler, '_mesh_is_injection_wrapper', False)
181
+ existing_wrapper = getattr(original_handler, '_mesh_injection_wrapper', None)
182
+
183
+ self.logger.debug(
184
+ f"🔍 Checking function {original_handler} at {hex(id(original_handler))}: "
185
+ f"is_wrapper={is_already_wrapper}, has_wrapper_ref={'yes' if existing_wrapper else 'no'}"
186
+ )
187
+
188
+ if is_already_wrapper:
189
+ self.logger.debug(
190
+ f"🔄 Function '{endpoint_name}' is already an injection wrapper from @mesh.route decorator"
191
+ )
192
+ wrapped_handler = original_handler # Use the function as-is
193
+ elif existing_wrapper:
194
+ self.logger.debug(f"🔍 Existing wrapper: {existing_wrapper} at {hex(id(existing_wrapper))}")
195
+ self.logger.debug(
196
+ f"🔄 Route '{endpoint_name}' already has injection wrapper from @mesh.route decorator, using existing wrapper"
197
+ )
198
+ wrapped_handler = existing_wrapper
199
+ else:
200
+ # Create dependency injection wrapper using existing engine
201
+ self.logger.debug(
202
+ f"🔧 Creating new injection wrapper for route '{endpoint_name}'"
203
+ )
204
+ try:
205
+ wrapped_handler = injector.create_injection_wrapper(
206
+ original_handler, dependency_names
207
+ )
208
+
209
+ # Preserve original handler metadata on wrapper
210
+ wrapped_handler._mesh_route_metadata = getattr(
211
+ original_handler, '_mesh_route_metadata', {}
212
+ )
213
+ wrapped_handler._original_handler = original_handler
214
+ wrapped_handler._mesh_dependencies = dependency_names
215
+ except Exception as e:
216
+ self.logger.error(f"Failed to create injection wrapper for {endpoint_name}: {e}")
217
+ return {
218
+ "status": "failed",
219
+ "reason": f"wrapper_creation_failed: {e}",
220
+ "dependency_count": len(dependency_names)
221
+ }
222
+
223
+ # CRITICAL FIX: Check if there are multiple wrapper instances for this function
224
+ # If so, use the one that actually receives dependency updates
225
+ from ...engine.dependency_injector import get_global_injector
226
+ injector = get_global_injector()
227
+
228
+ # Find all functions that depend on the first dependency of this route
229
+ if dependency_names:
230
+ first_dep = dependency_names[0] # Use first dependency to find all instances
231
+ affected_functions = injector._dependency_mapping.get(first_dep, set())
232
+ self.logger.debug(f"🎯 All functions with '{first_dep}' dependency: {list(affected_functions)}")
233
+
234
+ # Check if there are multiple instances and if so, prefer the one that's NOT __main__
235
+ if len(affected_functions) > 1:
236
+ non_main_functions = [f for f in affected_functions if not f.startswith('__main__.')]
237
+ if non_main_functions:
238
+ # Found a non-main instance, try to get that wrapper instead
239
+ preferred_func_id = non_main_functions[0] # Take first non-main
240
+ preferred_wrapper = injector._function_registry.get(preferred_func_id)
241
+ if preferred_wrapper:
242
+ self.logger.debug(
243
+ f"🔄 SWITCHING to preferred wrapper '{preferred_func_id}': "
244
+ f"{preferred_wrapper} at {hex(id(preferred_wrapper))}"
245
+ )
246
+ wrapped_handler = preferred_wrapper
247
+
248
+ # Find and replace the route handler in FastAPI
249
+ route_replaced = self._replace_route_handler(
250
+ app, path, methods, original_handler, wrapped_handler
251
+ )
252
+
253
+ if route_replaced:
254
+ self.logger.debug(
255
+ f"✅ Route '{endpoint_name}' integrated successfully with "
256
+ f"{len(dependency_names)} dependencies"
257
+ )
258
+ return {
259
+ "status": "integrated",
260
+ "dependency_count": len(dependency_names),
261
+ "dependencies": dependency_names,
262
+ "original_handler": original_handler,
263
+ "wrapped_handler": wrapped_handler
264
+ }
265
+ else:
266
+ self.logger.warning(
267
+ f"⚠️ Failed to find route to replace for '{endpoint_name}'"
268
+ )
269
+ return {
270
+ "status": "error",
271
+ "error": "route_not_found_for_replacement"
272
+ }
273
+
274
+ def _replace_route_handler(
275
+ self, app, path: str, methods: list, original_handler, wrapped_handler
276
+ ) -> bool:
277
+ """
278
+ Replace the route handler in FastAPI's router.
279
+
280
+ Args:
281
+ app: FastAPI application instance
282
+ path: Route path to find
283
+ methods: HTTP methods for the route
284
+ original_handler: Original handler function
285
+ wrapped_handler: New wrapped handler function
286
+
287
+ Returns:
288
+ True if replacement was successful, False otherwise
289
+ """
290
+ try:
291
+ # Find the matching route in FastAPI's router
292
+ for route in app.router.routes:
293
+ if (hasattr(route, 'endpoint') and
294
+ hasattr(route, 'path') and
295
+ hasattr(route, 'methods')):
296
+
297
+ # Match by path and endpoint function
298
+ if (route.path == path and
299
+ route.endpoint is original_handler):
300
+
301
+ # Replace the endpoint with our wrapped version
302
+ route.endpoint = wrapped_handler
303
+
304
+ self.logger.debug(
305
+ f"🔄 Replaced handler for {methods} {path}: "
306
+ f"{original_handler.__name__} -> wrapped version"
307
+ )
308
+ return True
309
+
310
+ # If we get here, we didn't find the route
311
+ self.logger.warning(
312
+ f"⚠️ Could not find route {methods} {path} to replace handler"
313
+ )
314
+ return False
315
+
316
+ except Exception as e:
317
+ self.logger.error(f"❌ Error replacing route handler: {e}")
318
+ return False