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,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
|