mcp-mesh 0.6.3__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.
@@ -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
- # Extract all dependencies from registered @mesh.route decorators
96
- all_route_dependencies = self._extract_all_route_dependencies(context)
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(f"📡 Sending API service payload to {registry_url}/heartbeat")
112
- self.logger.debug(f"🔍 POST payload: {json.dumps(api_service_payload, indent=2)}")
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(f"{registry_url} \"POST /heartbeat HTTP/1.1\" {response.status}")
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(f"❌ Registry error {response.status}: {response_text}")
127
- raise Exception(f"Registry returned {response.status}: {response_text}")
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(f"💚 API heartbeat successful for service '{service_id}'")
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": health_status if isinstance(health_status, dict) else {
211
- "status": health_status.status.value if hasattr(health_status, "status") and hasattr(health_status.status, "value") else str(getattr(health_status, "status", "healthy")),
212
- "timestamp": health_status.timestamp.isoformat() if hasattr(health_status, "timestamp") and hasattr(health_status.timestamp, "isoformat") else str(getattr(health_status, "timestamp", "")),
213
- "version": getattr(health_status, "version", "1.0.0"),
214
- "metadata": getattr(health_status, "metadata", {}),
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(self, context: dict[str, Any]) -> list[dict[str, Any]]:
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(f"🔍 api_service_metadata keys: {list(api_service_metadata.keys())}")
247
- self.logger.debug(f"🔍 route_capabilities count: {len(route_capabilities)}")
248
- self.logger.debug(f"🔍 route_capabilities: {route_capabilities}")
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
- "capability": dep,
263
- "tags": [] # No tags info available at this level
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.info(
269
- f"🔍 Extracted {len(all_dependencies)} unique dependencies from API startup: "
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("⚠️ No route dependencies found in context or FastAPI app")
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(self, fastapi_app: Any) -> list[dict[str, Any]]:
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
- "capability": capability,
315
- "tags": dep.get("tags", [])
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
- "capability": dep,
321
- "tags": []
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 []