mcp-mesh 0.6.4__py3-none-any.whl → 0.7.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 +1 -1
- _mcp_mesh/engine/decorator_registry.py +50 -11
- _mcp_mesh/engine/http_wrapper.py +10 -2
- _mcp_mesh/engine/mesh_llm_agent.py +98 -6
- _mcp_mesh/engine/unified_mcp_proxy.py +10 -2
- _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +82 -100
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +150 -96
- _mcp_mesh/pipeline/api_startup/route_integration.py +91 -92
- _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +7 -0
- _mcp_mesh/tracing/execution_tracer.py +41 -13
- {mcp_mesh-0.6.4.dist-info → mcp_mesh-0.7.0.dist-info}/METADATA +1 -1
- {mcp_mesh-0.6.4.dist-info → mcp_mesh-0.7.0.dist-info}/RECORD +15 -15
- mesh/decorators.py +43 -0
- {mcp_mesh-0.6.4.dist-info → mcp_mesh-0.7.0.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.6.4.dist-info → mcp_mesh-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -8,6 +8,7 @@ for FastAPI applications using @mesh.route decorators.
|
|
|
8
8
|
import logging
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
+
from ...engine.decorator_registry import DecoratorRegistry
|
|
11
12
|
from ..shared.base_step import PipelineStep
|
|
12
13
|
from ..shared.pipeline_types import PipelineResult
|
|
13
14
|
|
|
@@ -17,7 +18,7 @@ logger = logging.getLogger(__name__)
|
|
|
17
18
|
class APIHeartbeatSendStep(PipelineStep):
|
|
18
19
|
"""
|
|
19
20
|
Send API service heartbeat to registry.
|
|
20
|
-
|
|
21
|
+
|
|
21
22
|
Communicates service health status and registration information
|
|
22
23
|
to the registry for monitoring and discovery purposes.
|
|
23
24
|
"""
|
|
@@ -49,12 +50,11 @@ class APIHeartbeatSendStep(PipelineStep):
|
|
|
49
50
|
if not registry_wrapper:
|
|
50
51
|
error_msg = "No registry wrapper available for heartbeat"
|
|
51
52
|
self.logger.error(f"❌ {error_msg}")
|
|
52
|
-
|
|
53
|
+
|
|
53
54
|
from ..shared.pipeline_types import PipelineStatus
|
|
55
|
+
|
|
54
56
|
result = PipelineResult(
|
|
55
|
-
status=PipelineStatus.FAILED,
|
|
56
|
-
message=error_msg,
|
|
57
|
-
context=context
|
|
57
|
+
status=PipelineStatus.FAILED, message=error_msg, context=context
|
|
58
58
|
)
|
|
59
59
|
result.add_error(error_msg)
|
|
60
60
|
return result
|
|
@@ -62,12 +62,11 @@ class APIHeartbeatSendStep(PipelineStep):
|
|
|
62
62
|
if not health_status:
|
|
63
63
|
error_msg = "No health status available for heartbeat"
|
|
64
64
|
self.logger.error(f"❌ {error_msg}")
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
from ..shared.pipeline_types import PipelineStatus
|
|
67
|
+
|
|
67
68
|
result = PipelineResult(
|
|
68
|
-
status=PipelineStatus.FAILED,
|
|
69
|
-
message=error_msg,
|
|
70
|
-
context=context
|
|
69
|
+
status=PipelineStatus.FAILED, message=error_msg, context=context
|
|
71
70
|
)
|
|
72
71
|
result.add_error(error_msg)
|
|
73
72
|
return result
|
|
@@ -81,75 +80,111 @@ class APIHeartbeatSendStep(PipelineStep):
|
|
|
81
80
|
|
|
82
81
|
# Send heartbeat to registry using the same format as test_api_service.json
|
|
83
82
|
# Import json at the beginning
|
|
84
|
-
import aiohttp
|
|
85
83
|
import json
|
|
86
|
-
|
|
84
|
+
|
|
85
|
+
import aiohttp
|
|
86
|
+
|
|
87
87
|
try:
|
|
88
88
|
# For API services, send directly to registry using the format that works
|
|
89
|
-
# Get registry URL
|
|
89
|
+
# Get registry URL
|
|
90
90
|
registry_url = context.get("registry_url", "http://localhost:8000")
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
# Build the API service payload using actual dependencies from @mesh.route decorators
|
|
93
93
|
display_config = context.get("display_config", {})
|
|
94
|
-
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
|
|
95
|
+
# Build per-route tool entries using METHOD:path as unique identifiers
|
|
96
|
+
# This allows dependency resolution to map back to specific route wrappers
|
|
97
|
+
route_wrappers = DecoratorRegistry.get_all_route_wrappers()
|
|
98
|
+
tools_list = []
|
|
99
|
+
|
|
100
|
+
for route_id, route_info in route_wrappers.items():
|
|
101
|
+
dependencies = route_info.get("dependencies", [])
|
|
102
|
+
if dependencies: # Only include routes with dependencies
|
|
103
|
+
tools_list.append(
|
|
104
|
+
{
|
|
105
|
+
"function_name": route_id, # e.g., "GET:/api/v1/benchmark-services"
|
|
106
|
+
"dependencies": [
|
|
107
|
+
{"capability": dep, "tags": []}
|
|
108
|
+
for dep in dependencies
|
|
109
|
+
],
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Fallback to old behavior if no route wrappers registered yet
|
|
114
|
+
if not tools_list:
|
|
115
|
+
all_route_dependencies = self._extract_all_route_dependencies(
|
|
116
|
+
context
|
|
117
|
+
)
|
|
118
|
+
if all_route_dependencies:
|
|
119
|
+
tools_list.append(
|
|
120
|
+
{
|
|
121
|
+
"function_name": "api_endpoint_handler",
|
|
122
|
+
"dependencies": all_route_dependencies,
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
self.logger.debug(
|
|
127
|
+
f"Route wrappers registered: {list(route_wrappers.keys())}"
|
|
128
|
+
)
|
|
129
|
+
|
|
98
130
|
api_service_payload = {
|
|
99
131
|
"agent_id": service_id,
|
|
100
132
|
"agent_type": "api",
|
|
101
|
-
"tools":
|
|
102
|
-
{
|
|
103
|
-
"function_name": "api_endpoint_handler",
|
|
104
|
-
"dependencies": all_route_dependencies
|
|
105
|
-
}
|
|
106
|
-
],
|
|
133
|
+
"tools": tools_list,
|
|
107
134
|
"http_host": display_config.get("display_host", "127.0.0.1"),
|
|
108
|
-
"http_port": display_config.get("display_port", 8080)
|
|
135
|
+
"http_port": display_config.get("display_port", 8080),
|
|
109
136
|
}
|
|
110
|
-
|
|
111
|
-
self.logger.debug(
|
|
112
|
-
|
|
113
|
-
|
|
137
|
+
|
|
138
|
+
self.logger.debug(
|
|
139
|
+
f"Sending API service payload to {registry_url}/heartbeat"
|
|
140
|
+
)
|
|
141
|
+
|
|
114
142
|
try:
|
|
115
143
|
async with aiohttp.ClientSession() as session:
|
|
116
144
|
async with session.post(
|
|
117
145
|
f"{registry_url}/heartbeat",
|
|
118
146
|
headers={"Content-Type": "application/json"},
|
|
119
|
-
data=json.dumps(api_service_payload)
|
|
147
|
+
data=json.dumps(api_service_payload),
|
|
120
148
|
) as response:
|
|
121
|
-
self.logger.debug(
|
|
149
|
+
self.logger.debug(
|
|
150
|
+
f'{registry_url} "POST /heartbeat HTTP/1.1" {response.status}'
|
|
151
|
+
)
|
|
122
152
|
if response.status == 200:
|
|
123
153
|
heartbeat_response = await response.json()
|
|
124
154
|
else:
|
|
125
155
|
response_text = await response.text()
|
|
126
|
-
self.logger.error(
|
|
127
|
-
|
|
128
|
-
|
|
156
|
+
self.logger.error(
|
|
157
|
+
f"❌ Registry error {response.status}: {response_text}"
|
|
158
|
+
)
|
|
159
|
+
raise Exception(
|
|
160
|
+
f"Registry returned {response.status}: {response_text}"
|
|
161
|
+
)
|
|
162
|
+
|
|
129
163
|
except Exception as http_error:
|
|
130
164
|
self.logger.error(f"❌ HTTP request failed: {http_error}")
|
|
131
165
|
raise http_error
|
|
132
|
-
|
|
166
|
+
|
|
133
167
|
if heartbeat_response:
|
|
134
|
-
self.logger.info(
|
|
135
|
-
|
|
168
|
+
self.logger.info(
|
|
169
|
+
f"💚 API heartbeat successful for service '{service_id}'"
|
|
170
|
+
)
|
|
171
|
+
|
|
136
172
|
return PipelineResult(
|
|
137
173
|
message=f"API heartbeat sent for service {service_id}",
|
|
138
174
|
context={
|
|
139
175
|
"heartbeat_response": heartbeat_response,
|
|
140
176
|
"heartbeat_success": True,
|
|
141
177
|
"heartbeat_data": heartbeat_data,
|
|
142
|
-
}
|
|
178
|
+
},
|
|
143
179
|
)
|
|
144
180
|
else:
|
|
145
181
|
error_msg = f"Registry heartbeat failed for service {service_id}"
|
|
146
182
|
self.logger.warning(f"⚠️ {error_msg}")
|
|
147
|
-
|
|
183
|
+
|
|
148
184
|
from ..shared.pipeline_types import PipelineStatus
|
|
185
|
+
|
|
149
186
|
result = PipelineResult(
|
|
150
|
-
status=PipelineStatus.FAILED,
|
|
151
|
-
message=error_msg,
|
|
152
|
-
context=context
|
|
187
|
+
status=PipelineStatus.FAILED, message=error_msg, context=context
|
|
153
188
|
)
|
|
154
189
|
result.add_error(error_msg)
|
|
155
190
|
return result
|
|
@@ -157,12 +192,11 @@ class APIHeartbeatSendStep(PipelineStep):
|
|
|
157
192
|
except Exception as e:
|
|
158
193
|
error_msg = f"Registry communication failed: {e}"
|
|
159
194
|
self.logger.error(f"❌ {error_msg}")
|
|
160
|
-
|
|
195
|
+
|
|
161
196
|
from ..shared.pipeline_types import PipelineStatus
|
|
197
|
+
|
|
162
198
|
result = PipelineResult(
|
|
163
|
-
status=PipelineStatus.FAILED,
|
|
164
|
-
message=error_msg,
|
|
165
|
-
context=context
|
|
199
|
+
status=PipelineStatus.FAILED, message=error_msg, context=context
|
|
166
200
|
)
|
|
167
201
|
result.add_error(str(e))
|
|
168
202
|
return result
|
|
@@ -172,10 +206,9 @@ class APIHeartbeatSendStep(PipelineStep):
|
|
|
172
206
|
self.logger.error(f"❌ {error_msg}")
|
|
173
207
|
|
|
174
208
|
from ..shared.pipeline_types import PipelineStatus
|
|
209
|
+
|
|
175
210
|
result = PipelineResult(
|
|
176
|
-
status=PipelineStatus.FAILED,
|
|
177
|
-
message=error_msg,
|
|
178
|
-
context=context
|
|
211
|
+
status=PipelineStatus.FAILED, message=error_msg, context=context
|
|
179
212
|
)
|
|
180
213
|
result.add_error(str(e))
|
|
181
214
|
return result
|
|
@@ -190,7 +223,7 @@ class APIHeartbeatSendStep(PipelineStep):
|
|
|
190
223
|
app_version = context.get("app_version", "1.0.0")
|
|
191
224
|
routes_total = context.get("routes_total", 0)
|
|
192
225
|
routes_with_mesh = context.get("routes_with_mesh", 0)
|
|
193
|
-
|
|
226
|
+
|
|
194
227
|
# Get display configuration
|
|
195
228
|
display_config = context.get("display_config", {})
|
|
196
229
|
host = display_config.get("host", "0.0.0.0")
|
|
@@ -207,12 +240,26 @@ class APIHeartbeatSendStep(PipelineStep):
|
|
|
207
240
|
"total": routes_total,
|
|
208
241
|
"with_mesh": routes_with_mesh,
|
|
209
242
|
},
|
|
210
|
-
"health_status":
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
243
|
+
"health_status": (
|
|
244
|
+
health_status
|
|
245
|
+
if isinstance(health_status, dict)
|
|
246
|
+
else {
|
|
247
|
+
"status": (
|
|
248
|
+
health_status.status.value
|
|
249
|
+
if hasattr(health_status, "status")
|
|
250
|
+
and hasattr(health_status.status, "value")
|
|
251
|
+
else str(getattr(health_status, "status", "healthy"))
|
|
252
|
+
),
|
|
253
|
+
"timestamp": (
|
|
254
|
+
health_status.timestamp.isoformat()
|
|
255
|
+
if hasattr(health_status, "timestamp")
|
|
256
|
+
and hasattr(health_status.timestamp, "isoformat")
|
|
257
|
+
else str(getattr(health_status, "timestamp", ""))
|
|
258
|
+
),
|
|
259
|
+
"version": getattr(health_status, "version", "1.0.0"),
|
|
260
|
+
"metadata": getattr(health_status, "metadata", {}),
|
|
261
|
+
}
|
|
262
|
+
),
|
|
216
263
|
}
|
|
217
264
|
|
|
218
265
|
return heartbeat_data
|
|
@@ -222,19 +269,21 @@ class APIHeartbeatSendStep(PipelineStep):
|
|
|
222
269
|
return {
|
|
223
270
|
"service_id": service_id,
|
|
224
271
|
"service_type": "api",
|
|
225
|
-
"error": f"Failed to prepare heartbeat data: {e}"
|
|
272
|
+
"error": f"Failed to prepare heartbeat data: {e}",
|
|
226
273
|
}
|
|
227
274
|
|
|
228
|
-
def _extract_all_route_dependencies(
|
|
275
|
+
def _extract_all_route_dependencies(
|
|
276
|
+
self, context: dict[str, Any]
|
|
277
|
+
) -> list[dict[str, Any]]:
|
|
229
278
|
"""
|
|
230
279
|
Extract all unique dependencies from @mesh.route decorators in the FastAPI app.
|
|
231
|
-
|
|
280
|
+
|
|
232
281
|
This method looks at the actual route dependencies that were discovered during
|
|
233
282
|
the API startup pipeline and extracts them for registry registration.
|
|
234
|
-
|
|
283
|
+
|
|
235
284
|
Args:
|
|
236
285
|
context: Pipeline context containing FastAPI app and route information
|
|
237
|
-
|
|
286
|
+
|
|
238
287
|
Returns:
|
|
239
288
|
List of unique dependency objects in the format expected by registry
|
|
240
289
|
"""
|
|
@@ -242,15 +291,15 @@ class APIHeartbeatSendStep(PipelineStep):
|
|
|
242
291
|
# Try to get dependencies from startup context (preferred method)
|
|
243
292
|
api_service_metadata = context.get("api_service_metadata", {})
|
|
244
293
|
route_capabilities = api_service_metadata.get("capabilities", [])
|
|
245
|
-
|
|
246
|
-
self.logger.debug(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
294
|
+
|
|
295
|
+
self.logger.debug(
|
|
296
|
+
f"api_service_metadata keys: {list(api_service_metadata.keys())}"
|
|
297
|
+
)
|
|
298
|
+
|
|
250
299
|
# Extract dependencies from route capabilities
|
|
251
300
|
all_dependencies = []
|
|
252
301
|
seen_capabilities = set()
|
|
253
|
-
|
|
302
|
+
|
|
254
303
|
for route_capability in route_capabilities:
|
|
255
304
|
route_deps = route_capability.get("dependencies", [])
|
|
256
305
|
for dep in route_deps:
|
|
@@ -258,75 +307,80 @@ class APIHeartbeatSendStep(PipelineStep):
|
|
|
258
307
|
if isinstance(dep, str) and dep not in seen_capabilities:
|
|
259
308
|
seen_capabilities.add(dep)
|
|
260
309
|
# Convert to object format for registry
|
|
261
|
-
all_dependencies.append(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
310
|
+
all_dependencies.append(
|
|
311
|
+
{
|
|
312
|
+
"capability": dep,
|
|
313
|
+
"tags": [], # No tags info available at this level
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
|
|
266
317
|
# If we found dependencies from startup context, use them
|
|
267
318
|
if all_dependencies:
|
|
268
|
-
self.logger.
|
|
269
|
-
f"
|
|
319
|
+
self.logger.debug(
|
|
320
|
+
f"Extracted {len(all_dependencies)} unique dependencies from API startup: "
|
|
270
321
|
f"{[dep['capability'] for dep in all_dependencies]}"
|
|
271
322
|
)
|
|
272
323
|
return all_dependencies
|
|
273
|
-
|
|
324
|
+
|
|
274
325
|
# Fallback: try to extract directly from FastAPI app routes
|
|
275
326
|
fastapi_app = context.get("fastapi_app")
|
|
276
327
|
if fastapi_app:
|
|
277
328
|
return self._extract_dependencies_from_routes(fastapi_app)
|
|
278
|
-
|
|
329
|
+
|
|
279
330
|
# Final fallback: empty dependencies
|
|
280
|
-
self.logger.warning(
|
|
331
|
+
self.logger.warning(
|
|
332
|
+
"⚠️ No route dependencies found in context or FastAPI app"
|
|
333
|
+
)
|
|
281
334
|
return []
|
|
282
|
-
|
|
335
|
+
|
|
283
336
|
except Exception as e:
|
|
284
337
|
self.logger.error(f"❌ Failed to extract route dependencies: {e}")
|
|
285
338
|
return []
|
|
286
|
-
|
|
287
|
-
def _extract_dependencies_from_routes(
|
|
339
|
+
|
|
340
|
+
def _extract_dependencies_from_routes(
|
|
341
|
+
self, fastapi_app: Any
|
|
342
|
+
) -> list[dict[str, Any]]:
|
|
288
343
|
"""
|
|
289
344
|
Fallback method to extract dependencies directly from FastAPI route metadata.
|
|
290
|
-
|
|
345
|
+
|
|
291
346
|
Args:
|
|
292
347
|
fastapi_app: FastAPI application instance
|
|
293
|
-
|
|
348
|
+
|
|
294
349
|
Returns:
|
|
295
350
|
List of unique dependency objects
|
|
296
351
|
"""
|
|
297
352
|
try:
|
|
298
353
|
all_dependencies = []
|
|
299
354
|
seen_capabilities = set()
|
|
300
|
-
|
|
355
|
+
|
|
301
356
|
routes = getattr(fastapi_app, "routes", [])
|
|
302
357
|
for route in routes:
|
|
303
358
|
endpoint = getattr(route, "endpoint", None)
|
|
304
359
|
if endpoint and hasattr(endpoint, "_mesh_route_metadata"):
|
|
305
360
|
metadata = endpoint._mesh_route_metadata
|
|
306
361
|
route_deps = metadata.get("dependencies", [])
|
|
307
|
-
|
|
362
|
+
|
|
308
363
|
for dep in route_deps:
|
|
309
364
|
if isinstance(dep, dict):
|
|
310
365
|
capability = dep.get("capability")
|
|
311
366
|
if capability and capability not in seen_capabilities:
|
|
312
367
|
seen_capabilities.add(capability)
|
|
313
|
-
all_dependencies.append(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
368
|
+
all_dependencies.append(
|
|
369
|
+
{
|
|
370
|
+
"capability": capability,
|
|
371
|
+
"tags": dep.get("tags", []),
|
|
372
|
+
}
|
|
373
|
+
)
|
|
317
374
|
elif isinstance(dep, str) and dep not in seen_capabilities:
|
|
318
375
|
seen_capabilities.add(dep)
|
|
319
|
-
all_dependencies.append({
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
self.logger.info(
|
|
325
|
-
f"🔍 Extracted {len(all_dependencies)} unique dependencies from FastAPI routes: "
|
|
376
|
+
all_dependencies.append({"capability": dep, "tags": []})
|
|
377
|
+
|
|
378
|
+
self.logger.debug(
|
|
379
|
+
f"Extracted {len(all_dependencies)} unique dependencies from FastAPI routes: "
|
|
326
380
|
f"{[dep['capability'] for dep in all_dependencies]}"
|
|
327
381
|
)
|
|
328
382
|
return all_dependencies
|
|
329
|
-
|
|
383
|
+
|
|
330
384
|
except Exception as e:
|
|
331
385
|
self.logger.error(f"❌ Failed to extract dependencies from routes: {e}")
|
|
332
|
-
return []
|
|
386
|
+
return []
|