mcp-mesh 0.7.21__py3-none-any.whl → 0.8.0b1__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 (121) hide show
  1. _mcp_mesh/__init__.py +1 -1
  2. _mcp_mesh/engine/dependency_injector.py +4 -6
  3. _mcp_mesh/engine/http_wrapper.py +69 -10
  4. _mcp_mesh/engine/mesh_llm_agent.py +4 -7
  5. _mcp_mesh/engine/mesh_llm_agent_injector.py +2 -1
  6. _mcp_mesh/engine/provider_handlers/__init__.py +14 -1
  7. _mcp_mesh/engine/provider_handlers/base_provider_handler.py +114 -8
  8. _mcp_mesh/engine/provider_handlers/claude_handler.py +15 -57
  9. _mcp_mesh/engine/provider_handlers/gemini_handler.py +181 -0
  10. _mcp_mesh/engine/provider_handlers/openai_handler.py +8 -63
  11. _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +16 -10
  12. _mcp_mesh/engine/response_parser.py +61 -15
  13. _mcp_mesh/engine/unified_mcp_proxy.py +18 -34
  14. _mcp_mesh/pipeline/__init__.py +9 -20
  15. _mcp_mesh/pipeline/api_heartbeat/__init__.py +12 -7
  16. _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +23 -49
  17. _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +425 -0
  18. _mcp_mesh/pipeline/api_startup/api_pipeline.py +7 -9
  19. _mcp_mesh/pipeline/api_startup/api_server_setup.py +91 -70
  20. _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +22 -23
  21. _mcp_mesh/pipeline/api_startup/middleware_integration.py +32 -24
  22. _mcp_mesh/pipeline/api_startup/route_collection.py +2 -4
  23. _mcp_mesh/pipeline/mcp_heartbeat/__init__.py +5 -17
  24. _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +695 -0
  25. _mcp_mesh/pipeline/mcp_startup/__init__.py +2 -5
  26. _mcp_mesh/pipeline/mcp_startup/configuration.py +1 -1
  27. _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +5 -6
  28. _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
  29. _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +21 -9
  30. _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +3 -8
  31. _mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -2
  32. _mcp_mesh/reload.py +1 -3
  33. _mcp_mesh/shared/__init__.py +2 -8
  34. _mcp_mesh/shared/config_resolver.py +124 -80
  35. _mcp_mesh/shared/defaults.py +89 -14
  36. _mcp_mesh/shared/fastapi_middleware_manager.py +149 -91
  37. _mcp_mesh/shared/host_resolver.py +8 -46
  38. _mcp_mesh/shared/server_discovery.py +115 -86
  39. _mcp_mesh/shared/simple_shutdown.py +44 -86
  40. _mcp_mesh/tracing/execution_tracer.py +2 -6
  41. _mcp_mesh/tracing/redis_metadata_publisher.py +24 -79
  42. _mcp_mesh/tracing/trace_context_helper.py +3 -13
  43. _mcp_mesh/tracing/utils.py +29 -15
  44. _mcp_mesh/utils/fastmcp_schema_extractor.py +2 -1
  45. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/METADATA +2 -1
  46. mcp_mesh-0.8.0b1.dist-info/RECORD +85 -0
  47. mesh/__init__.py +2 -1
  48. mesh/decorators.py +89 -5
  49. _mcp_mesh/generated/.openapi-generator/FILES +0 -50
  50. _mcp_mesh/generated/.openapi-generator/VERSION +0 -1
  51. _mcp_mesh/generated/.openapi-generator-ignore +0 -15
  52. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -90
  53. _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -6
  54. _mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -1088
  55. _mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -764
  56. _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -303
  57. _mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -798
  58. _mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -21
  59. _mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -577
  60. _mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -217
  61. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -55
  62. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -158
  63. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -126
  64. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -139
  65. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -92
  66. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -103
  67. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -136
  68. _mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -100
  69. _mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -107
  70. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -112
  71. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -103
  72. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -105
  73. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -103
  74. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +0 -106
  75. _mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -91
  76. _mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -103
  77. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -101
  78. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -111
  79. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -117
  80. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +0 -93
  81. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider_resolution_info.py +0 -106
  82. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +0 -109
  83. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +0 -139
  84. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +0 -91
  85. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +0 -101
  86. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_resolution_info.py +0 -120
  87. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -112
  88. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -129
  89. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -153
  90. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -101
  91. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -93
  92. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -107
  93. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -117
  94. _mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -119
  95. _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +0 -110
  96. _mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -93
  97. _mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -92
  98. _mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -93
  99. _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -106
  100. _mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
  101. _mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -259
  102. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -418
  103. _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -117
  104. _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -140
  105. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -243
  106. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -311
  107. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -386
  108. _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -104
  109. _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -396
  110. _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -116
  111. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -311
  112. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -282
  113. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -98
  114. _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +0 -84
  115. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +0 -264
  116. _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -79
  117. _mcp_mesh/pipeline/shared/registry_connection.py +0 -80
  118. _mcp_mesh/shared/registry_client_wrapper.py +0 -515
  119. mcp_mesh-0.7.21.dist-info/RECORD +0 -152
  120. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/WHEEL +0 -0
  121. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -8,8 +8,8 @@ outside the pipeline, such as by immediate uvicorn start in decorators.
8
8
  import gc
9
9
  import logging
10
10
  import socket
11
- from typing import Any, Dict, List, Optional, Tuple
12
11
  import threading
12
+ from typing import Any, Dict, List, Optional, Tuple
13
13
 
14
14
  logger = logging.getLogger(__name__)
15
15
 
@@ -21,10 +21,10 @@ class ServerDiscoveryUtil:
21
21
  def discover_fastapi_instances() -> Dict[str, Dict[str, Any]]:
22
22
  """
23
23
  Discover FastAPI application instances in the Python runtime.
24
-
24
+
25
25
  Uses intelligent deduplication to handle standard uvicorn patterns where
26
26
  the same app might be imported multiple times (e.g., "module:app" pattern).
27
-
27
+
28
28
  Returns:
29
29
  Dict mapping app_id -> app_info where app_info contains:
30
30
  - 'instance': The FastAPI app instance
@@ -34,7 +34,7 @@ class ServerDiscoveryUtil:
34
34
  """
35
35
  fastapi_apps = {}
36
36
  seen_apps = {} # For deduplication: title -> app_info
37
-
37
+
38
38
  try:
39
39
  # Import FastAPI here to avoid dependency if not used
40
40
  from fastapi import FastAPI
@@ -55,22 +55,24 @@ class ServerDiscoveryUtil:
55
55
  version = getattr(obj, "version", "unknown")
56
56
  routes = ServerDiscoveryUtil._extract_route_info(obj)
57
57
  route_count = len(routes)
58
-
58
+
59
59
  # Create a signature for deduplication
60
60
  app_signature = (title, version, route_count)
61
-
61
+
62
62
  # Check if we've seen an identical app
63
63
  if app_signature in seen_apps:
64
64
  existing_app = seen_apps[app_signature]
65
65
  # Compare route details to ensure they're truly identical
66
66
  existing_routes = existing_app["routes"]
67
-
68
- if ServerDiscoveryUtil._routes_are_identical(routes, existing_routes):
67
+
68
+ if ServerDiscoveryUtil._routes_are_identical(
69
+ routes, existing_routes
70
+ ):
69
71
  logger.debug(
70
72
  f"Skipping duplicate FastAPI app: '{title}' (same title, version, and routes)"
71
73
  )
72
74
  continue # Skip this duplicate
73
-
75
+
74
76
  # This is a unique app, add it
75
77
  app_id = f"app_{id(obj)}"
76
78
  app_info = {
@@ -80,17 +82,19 @@ class ServerDiscoveryUtil:
80
82
  "routes": routes,
81
83
  "module": ServerDiscoveryUtil._get_app_module(obj),
82
84
  "object_id": id(obj),
83
- "router_routes_count": len(obj.router.routes) if hasattr(obj, 'router') else 0,
85
+ "router_routes_count": (
86
+ len(obj.router.routes) if hasattr(obj, "router") else 0
87
+ ),
84
88
  }
85
-
89
+
86
90
  fastapi_apps[app_id] = app_info
87
91
  seen_apps[app_signature] = app_info
88
-
92
+
89
93
  logger.debug(
90
94
  f"Found FastAPI app: '{title}' (module: {app_info['module']}) with "
91
95
  f"{len(routes)} routes"
92
96
  )
93
-
97
+
94
98
  except Exception as e:
95
99
  logger.warning(f"Error analyzing FastAPI app: {e}")
96
100
  continue
@@ -101,7 +105,7 @@ class ServerDiscoveryUtil:
101
105
  def discover_running_servers() -> List[Dict[str, Any]]:
102
106
  """
103
107
  Discover running uvicorn servers by scanning threads and checking port bindings.
104
-
108
+
105
109
  Returns:
106
110
  List of server info dictionaries containing:
107
111
  - 'type': 'uvicorn' or 'unknown'
@@ -111,183 +115,208 @@ class ServerDiscoveryUtil:
111
115
  - 'app': FastAPI app if discoverable
112
116
  """
113
117
  running_servers = []
114
-
118
+
115
119
  # Look for uvicorn server threads
116
120
  for thread in threading.enumerate():
117
- if hasattr(thread, '_target'):
121
+ if hasattr(thread, "_target"):
118
122
  # Check if thread target looks like a uvicorn server
119
- target_name = getattr(thread._target, '__name__', '') if thread._target else ''
120
- if 'server' in target_name.lower() or 'uvicorn' in target_name.lower():
123
+ target_name = (
124
+ getattr(thread._target, "__name__", "") if thread._target else ""
125
+ )
126
+ if "server" in target_name.lower() or "uvicorn" in target_name.lower():
121
127
  server_info = {
122
- 'type': 'uvicorn',
123
- 'thread': thread,
124
- 'target_name': target_name,
125
- 'daemon': thread.daemon,
126
- 'alive': thread.is_alive(),
128
+ "type": "uvicorn",
129
+ "thread": thread,
130
+ "target_name": target_name,
131
+ "daemon": thread.daemon,
132
+ "alive": thread.is_alive(),
127
133
  }
128
-
134
+
129
135
  # Try to extract server details from thread
130
- server_details = ServerDiscoveryUtil._extract_server_details_from_thread(thread)
136
+ server_details = (
137
+ ServerDiscoveryUtil._extract_server_details_from_thread(thread)
138
+ )
131
139
  server_info.update(server_details)
132
-
140
+
133
141
  running_servers.append(server_info)
134
- logger.debug(f"Found running server thread: {target_name} (daemon={thread.daemon})")
135
-
142
+ logger.debug(
143
+ f"Found running server thread: {target_name} (daemon={thread.daemon})"
144
+ )
145
+
136
146
  # Also check for bound ports that might indicate running servers
137
147
  bound_ports = ServerDiscoveryUtil._discover_bound_ports()
138
148
  for port_info in bound_ports:
139
149
  # Only add if we haven't already found this port via thread discovery
140
- existing_ports = [s.get('port') for s in running_servers if s.get('port')]
141
- if port_info['port'] not in existing_ports:
142
- port_info['type'] = 'unknown'
150
+ existing_ports = [s.get("port") for s in running_servers if s.get("port")]
151
+ if port_info["port"] not in existing_ports:
152
+ port_info["type"] = "unknown"
143
153
  running_servers.append(port_info)
144
- logger.debug(f"Found bound port: {port_info['host']}:{port_info['port']}")
145
-
154
+ logger.debug(
155
+ f"Found bound port: {port_info['host']}:{port_info['port']}"
156
+ )
157
+
146
158
  return running_servers
147
159
 
148
160
  @staticmethod
149
161
  def _extract_server_details_from_thread(thread) -> Dict[str, Any]:
150
162
  """Extract server details from a thread if possible."""
151
163
  details = {}
152
-
164
+
153
165
  try:
154
166
  # Try to access thread local variables or target args
155
- if hasattr(thread, '_args') and thread._args:
167
+ if hasattr(thread, "_args") and thread._args:
156
168
  # Some uvicorn servers might have args with host/port
157
169
  args = thread._args
158
170
  if len(args) >= 2:
159
171
  # Common pattern: (app, host, port) or similar
160
- if isinstance(args[0], str) and ':' in args[0]:
172
+ if isinstance(args[0], str) and ":" in args[0]:
161
173
  # Might be "host:port" format
162
174
  try:
163
- host, port = args[0].split(':')
164
- details['host'] = host
165
- details['port'] = int(port)
175
+ host, port = args[0].split(":")
176
+ details["host"] = host
177
+ details["port"] = int(port)
166
178
  except (ValueError, IndexError):
167
179
  pass
168
-
180
+
169
181
  # Try to find FastAPI app in thread target or args
170
- if hasattr(thread, '_target') and thread._target:
182
+ if hasattr(thread, "_target") and thread._target:
171
183
  # Check if target has app attribute or if it's in closure
172
184
  target = thread._target
173
- if hasattr(target, '__closure__') and target.__closure__:
185
+ if hasattr(target, "__closure__") and target.__closure__:
174
186
  for cell in target.__closure__:
175
187
  try:
176
188
  cell_contents = cell.cell_contents
177
189
  from fastapi import FastAPI
190
+
178
191
  if isinstance(cell_contents, FastAPI):
179
- details['app'] = cell_contents
180
- details['app_title'] = getattr(cell_contents, 'title', 'Unknown')
192
+ details["app"] = cell_contents
193
+ details["app_title"] = getattr(
194
+ cell_contents, "title", "Unknown"
195
+ )
181
196
  break
182
197
  except (ImportError, AttributeError):
183
198
  continue
184
-
199
+
185
200
  except Exception as e:
186
201
  logger.debug(f"Could not extract server details from thread: {e}")
187
-
202
+
188
203
  return details
189
204
 
190
205
  @staticmethod
191
206
  def _discover_bound_ports() -> List[Dict[str, Any]]:
192
207
  """Discover ports that are currently bound by this process."""
193
208
  bound_ports = []
194
-
209
+
195
210
  try:
196
211
  # Common port ranges for web servers
197
212
  common_ports = [8000, 8080, 8090, 9090, 9091, 3000, 3001, 4000, 5000]
198
-
213
+
199
214
  for port in common_ports:
200
- for host in ['127.0.0.1', '0.0.0.0', 'localhost']:
215
+ for host in ["127.0.0.1", "0.0.0.0", "localhost"]:
201
216
  try:
202
217
  # Try to connect to see if port is bound
203
218
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
204
219
  sock.settimeout(0.1) # Very short timeout
205
220
  result = sock.connect_ex((host, port))
206
221
  sock.close()
207
-
222
+
208
223
  if result == 0: # Connection successful = port is bound
209
- bound_ports.append({
210
- 'host': host,
211
- 'port': port,
212
- 'status': 'bound'
213
- })
224
+ bound_ports.append(
225
+ {"host": host, "port": port, "status": "bound"}
226
+ )
214
227
  break # Don't check other hosts for same port
215
228
  except Exception:
216
229
  continue
217
-
230
+
218
231
  except Exception as e:
219
232
  logger.debug(f"Error discovering bound ports: {e}")
220
-
233
+
221
234
  return bound_ports
222
235
 
223
236
  @staticmethod
224
- def find_server_on_port(target_port: int, target_host: str = None) -> Optional[Dict[str, Any]]:
237
+ def find_server_on_port(
238
+ target_port: int, target_host: str = None
239
+ ) -> Optional[Dict[str, Any]]:
225
240
  """
226
241
  Find if there's already a server running on the specified port.
227
-
242
+
228
243
  Args:
229
244
  target_port: Port to check
230
245
  target_host: Host to check (optional)
231
-
246
+
232
247
  Returns:
233
248
  Server info dict if found, None otherwise
234
249
  """
235
250
  running_servers = ServerDiscoveryUtil.discover_running_servers()
236
-
251
+
237
252
  for server in running_servers:
238
- server_port = server.get('port')
239
- server_host = server.get('host')
240
-
253
+ server_port = server.get("port")
254
+ server_host = server.get("host")
255
+
241
256
  # Check port match
242
257
  if server_port == target_port:
243
258
  # If target_host is specified, check host match too
244
- if target_host is None or server_host == target_host or server_host in ['0.0.0.0', '127.0.0.1']:
245
- logger.info(f"🔍 DISCOVERY: Found existing server on {server_host}:{server_port}")
259
+ if (
260
+ target_host is None
261
+ or server_host == target_host
262
+ or server_host in ["0.0.0.0", "127.0.0.1"]
263
+ ):
264
+ logger.info(
265
+ f"🔍 DISCOVERY: Found existing server on {server_host}:{server_port}"
266
+ )
246
267
  return server
247
-
268
+
248
269
  logger.debug(f"🔍 DISCOVERY: No existing server found on port {target_port}")
249
270
  return None
250
271
 
251
272
  @staticmethod
252
- def _routes_are_identical(routes1: List[Dict[str, Any]], routes2: List[Dict[str, Any]]) -> bool:
273
+ def _routes_are_identical(
274
+ routes1: List[Dict[str, Any]], routes2: List[Dict[str, Any]]
275
+ ) -> bool:
253
276
  """Compare two route lists to see if they're identical."""
254
277
  if len(routes1) != len(routes2):
255
278
  return False
256
-
279
+
257
280
  # Create comparable signatures for each route
258
281
  def route_signature(route):
259
282
  return (
260
- tuple(sorted(route.get('methods', []))), # Sort methods for consistent comparison
261
- route.get('path', ''),
262
- route.get('endpoint_name', '')
283
+ tuple(
284
+ sorted(route.get("methods", []))
285
+ ), # Sort methods for consistent comparison
286
+ route.get("path", ""),
287
+ route.get("endpoint_name", ""),
263
288
  )
264
-
289
+
265
290
  # Sort routes by signature for consistent comparison
266
291
  sig1 = sorted([route_signature(r) for r in routes1])
267
292
  sig2 = sorted([route_signature(r) for r in routes2])
268
-
293
+
269
294
  return sig1 == sig2
270
295
 
271
296
  @staticmethod
272
297
  def _extract_route_info(app) -> List[Dict[str, Any]]:
273
298
  """Extract route information from FastAPI app without modifying it."""
274
299
  routes = []
275
-
300
+
276
301
  try:
277
302
  for route in app.router.routes:
278
- if hasattr(route, 'endpoint') and hasattr(route, 'path'):
303
+ if hasattr(route, "endpoint") and hasattr(route, "path"):
279
304
  route_info = {
280
305
  "path": route.path,
281
- "methods": list(route.methods) if hasattr(route, 'methods') else [],
306
+ "methods": (
307
+ list(route.methods) if hasattr(route, "methods") else []
308
+ ),
282
309
  "endpoint": route.endpoint,
283
- "endpoint_name": getattr(route.endpoint, '__name__', 'unknown'),
284
- "has_mesh_route": hasattr(route.endpoint, '_mesh_route_metadata'),
310
+ "endpoint_name": getattr(route.endpoint, "__name__", "unknown"),
311
+ "has_mesh_route": hasattr(
312
+ route.endpoint, "_mesh_route_metadata"
313
+ ),
285
314
  }
286
315
  routes.append(route_info)
287
-
316
+
288
317
  except Exception as e:
289
318
  logger.warning(f"Error extracting route info: {e}")
290
-
319
+
291
320
  return routes
292
321
 
293
322
  @staticmethod
@@ -297,16 +326,16 @@ class ServerDiscoveryUtil:
297
326
  # Try to get module from the app's stack frame when it was created
298
327
  # This is best-effort - may not always work
299
328
  import inspect
300
-
329
+
301
330
  frame = inspect.currentframe()
302
331
  while frame:
303
332
  frame_globals = frame.f_globals
304
333
  for name, obj in frame_globals.items():
305
334
  if obj is app:
306
- return frame_globals.get('__name__', 'unknown')
335
+ return frame_globals.get("__name__", "unknown")
307
336
  frame = frame.f_back
308
-
337
+
309
338
  except Exception:
310
339
  pass
311
-
312
- return None
340
+
341
+ return None
@@ -2,9 +2,9 @@
2
2
  Simple shutdown coordination for MCP Mesh agents.
3
3
 
4
4
  Provides clean shutdown via FastAPI lifespan events and basic signal handling.
5
+ The Rust core handles actual deregistration from the registry.
5
6
  """
6
7
 
7
- import asyncio
8
8
  import logging
9
9
  import signal
10
10
  from contextlib import asynccontextmanager
@@ -14,7 +14,12 @@ logger = logging.getLogger(__name__)
14
14
 
15
15
 
16
16
  class SimpleShutdownCoordinator:
17
- """Lightweight shutdown coordination using FastAPI lifespan."""
17
+ """Lightweight shutdown coordination using FastAPI lifespan.
18
+
19
+ The Rust core handles registry deregistration automatically when
20
+ handle.shutdown() is called. This coordinator just manages the
21
+ shutdown signal flow between Python and Rust.
22
+ """
18
23
 
19
24
  def __init__(self):
20
25
  self._shutdown_requested = False
@@ -23,7 +28,7 @@ class SimpleShutdownCoordinator:
23
28
  self._shutdown_complete = False # Flag to prevent race conditions
24
29
 
25
30
  def set_shutdown_context(self, registry_url: str, agent_id: str) -> None:
26
- """Set context for shutdown cleanup."""
31
+ """Set context for shutdown (used for logging)."""
27
32
  self._registry_url = registry_url
28
33
  self._agent_id = agent_id
29
34
  logger.debug(
@@ -54,61 +59,20 @@ class SimpleShutdownCoordinator:
54
59
  self._shutdown_complete = True
55
60
  logger.debug("🏁 Shutdown marked as complete")
56
61
 
57
- async def perform_registry_cleanup(self) -> None:
58
- """Perform registry cleanup by calling DELETE /agents/{agent_id}."""
59
- # Try to get the actual agent_id from DecoratorRegistry if available
60
- actual_agent_id = self._agent_id
61
- try:
62
- from _mcp_mesh.engine.decorator_registry import DecoratorRegistry
63
-
64
- agent_config = DecoratorRegistry.get_resolved_agent_config()
65
- if agent_config and "agent_id" in agent_config:
66
- resolved_agent_id = agent_config["agent_id"]
67
- if resolved_agent_id and resolved_agent_id != "unknown":
68
- actual_agent_id = resolved_agent_id
69
- logger.debug(
70
- f"🔧 Using resolved agent_id from DecoratorRegistry: {actual_agent_id}"
71
- )
72
- except Exception as e:
73
- logger.debug(f"Could not get agent_id from DecoratorRegistry: {e}")
74
-
75
- if (
76
- not self._registry_url
77
- or not actual_agent_id
78
- or actual_agent_id == "unknown"
79
- ):
80
- logger.warning(
81
- f"⚠️ Missing registry URL or agent ID for cleanup: registry_url={self._registry_url}, agent_id={actual_agent_id}"
82
- )
83
- return
84
-
85
- try:
86
- from _mcp_mesh.generated.mcp_mesh_registry_client.api_client import (
87
- ApiClient,
88
- )
89
- from _mcp_mesh.generated.mcp_mesh_registry_client.configuration import (
90
- Configuration,
91
- )
92
- from _mcp_mesh.shared.registry_client_wrapper import RegistryClientWrapper
93
-
94
- config = Configuration(host=self._registry_url)
95
- api_client = ApiClient(configuration=config)
96
- registry_wrapper = RegistryClientWrapper(api_client)
97
-
98
- success = await registry_wrapper.unregister_agent(actual_agent_id)
99
- if success:
100
- logger.info(f"✅ Agent '{actual_agent_id}' unregistered from registry")
101
- self.mark_shutdown_complete()
102
- else:
103
- logger.warning(f"⚠️ Failed to unregister agent '{actual_agent_id}'")
104
- self.mark_shutdown_complete() # Mark complete even on failure to prevent loops
105
-
106
- except Exception as e:
107
- logger.error(f"❌ Registry cleanup error: {e}")
108
- self.mark_shutdown_complete() # Mark complete even on error to prevent loops
62
+ def request_shutdown(self) -> None:
63
+ """Request shutdown (called when lifespan exits)."""
64
+ self._shutdown_requested = True
65
+ agent_id = self._agent_id or "<unknown>"
66
+ logger.info(f"🔄 Shutdown requested for agent '{agent_id}'")
109
67
 
110
68
  def create_shutdown_lifespan(self, original_lifespan=None):
111
- """Create lifespan function that includes registry cleanup."""
69
+ """Create lifespan function that signals shutdown on exit.
70
+
71
+ The Rust core will handle actual deregistration when it receives
72
+ the shutdown signal via handle.shutdown().
73
+ """
74
+ # Capture agent_id at creation time with fallback for None
75
+ agent_id = self._agent_id or "<unknown>"
112
76
 
113
77
  @asynccontextmanager
114
78
  async def shutdown_lifespan(app):
@@ -120,10 +84,14 @@ class SimpleShutdownCoordinator:
120
84
  else:
121
85
  yield
122
86
 
123
- # Shutdown phase
124
- logger.info("🔄 FastAPI shutdown initiated, performing registry cleanup...")
125
- await self.perform_registry_cleanup()
126
- logger.info("🏁 Registry cleanup completed")
87
+ # Shutdown phase - just signal, Rust handles deregistration
88
+ logger.info(
89
+ f"🔄 FastAPI shutdown initiated for agent '{agent_id}', "
90
+ "Rust core will handle deregistration"
91
+ )
92
+ self.request_shutdown()
93
+ self.mark_shutdown_complete()
94
+ logger.info("🏁 Shutdown signaled")
127
95
 
128
96
  return shutdown_lifespan
129
97
 
@@ -164,12 +132,15 @@ def start_blocking_loop_with_shutdown_support(thread) -> None:
164
132
  """
165
133
  Keep main thread alive while uvicorn in the thread handles requests.
166
134
 
167
- Install signal handlers in main thread for proper registry cleanup since
135
+ Install signal handlers in main thread for proper shutdown signaling since
168
136
  signals to threads can be unreliable for FastAPI lifespan shutdown.
137
+
138
+ Note: The Rust core handles registry deregistration automatically when
139
+ handle.shutdown() is called from the heartbeat task.
169
140
  """
170
- logger.info("🔒 MAIN THREAD: Installing signal handlers for registry cleanup")
141
+ logger.info("🔒 MAIN THREAD: Installing signal handlers")
171
142
 
172
- # Install signal handlers for proper registry cleanup
143
+ # Install signal handlers
173
144
  _simple_shutdown_coordinator.install_signal_handlers()
174
145
 
175
146
  logger.info(
@@ -184,34 +155,21 @@ def start_blocking_loop_with_shutdown_support(thread) -> None:
184
155
  # Check if shutdown was requested via signal
185
156
  if _simple_shutdown_coordinator.is_shutdown_requested():
186
157
  logger.info(
187
- "🔄 MAIN THREAD: Shutdown requested, performing registry cleanup..."
158
+ "🔄 MAIN THREAD: Shutdown requested, signaling heartbeat to stop..."
159
+ )
160
+ # Mark shutdown complete so heartbeat task will call handle.shutdown()
161
+ # which triggers Rust core to deregister from registry
162
+ _simple_shutdown_coordinator.mark_shutdown_complete()
163
+ logger.info(
164
+ "🏁 MAIN THREAD: Shutdown signaled, Rust core will handle deregistration"
188
165
  )
189
-
190
- # Perform registry cleanup in main thread
191
- import asyncio
192
-
193
- try:
194
- # Run cleanup in main thread
195
- asyncio.run(_simple_shutdown_coordinator.perform_registry_cleanup())
196
- except Exception as e:
197
- logger.error(f"❌ Registry cleanup error: {e}")
198
-
199
- logger.info("🏁 MAIN THREAD: Registry cleanup completed, exiting")
200
166
  break
201
167
 
202
168
  except KeyboardInterrupt:
169
+ logger.info("🔄 MAIN THREAD: KeyboardInterrupt received, signaling shutdown...")
170
+ _simple_shutdown_coordinator.mark_shutdown_complete()
203
171
  logger.info(
204
- "🔄 MAIN THREAD: KeyboardInterrupt received, performing registry cleanup..."
172
+ "🏁 MAIN THREAD: Shutdown signaled, Rust core will handle deregistration"
205
173
  )
206
174
 
207
- # Perform registry cleanup on Ctrl+C
208
- import asyncio
209
-
210
- try:
211
- asyncio.run(_simple_shutdown_coordinator.perform_registry_cleanup())
212
- except Exception as e:
213
- logger.error(f"❌ Registry cleanup error: {e}")
214
-
215
- logger.info("🏁 MAIN THREAD: Registry cleanup completed")
216
-
217
175
  logger.info("🏁 MAIN THREAD: Uvicorn thread completed")
@@ -10,12 +10,8 @@ from collections.abc import Callable
10
10
  from typing import Any, Optional
11
11
 
12
12
  # Import shared utilities at module level to avoid circular imports during execution
13
- from .utils import (
14
- generate_span_id,
15
- get_agent_metadata_with_fallback,
16
- is_tracing_enabled,
17
- publish_trace_with_fallback,
18
- )
13
+ from .utils import (generate_span_id, get_agent_metadata_with_fallback,
14
+ is_tracing_enabled, publish_trace_with_fallback)
19
15
 
20
16
  logger = logging.getLogger(__name__)
21
17