mcp-mesh 0.7.12__py3-none-any.whl → 0.7.14__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 (41) hide show
  1. _mcp_mesh/__init__.py +1 -1
  2. _mcp_mesh/engine/__init__.py +1 -22
  3. _mcp_mesh/engine/async_mcp_client.py +88 -25
  4. _mcp_mesh/engine/decorator_registry.py +10 -9
  5. _mcp_mesh/engine/dependency_injector.py +64 -53
  6. _mcp_mesh/engine/mesh_llm_agent.py +119 -5
  7. _mcp_mesh/engine/mesh_llm_agent_injector.py +30 -0
  8. _mcp_mesh/engine/session_aware_client.py +3 -3
  9. _mcp_mesh/engine/unified_mcp_proxy.py +82 -90
  10. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -89
  11. _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +3 -3
  12. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +30 -28
  13. _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +16 -18
  14. _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +5 -5
  15. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +3 -3
  16. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +6 -6
  17. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +1 -1
  18. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +15 -11
  19. _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +3 -3
  20. _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +37 -268
  21. _mcp_mesh/pipeline/mcp_startup/lifespan_factory.py +142 -0
  22. _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +57 -93
  23. _mcp_mesh/pipeline/shared/registry_connection.py +1 -1
  24. _mcp_mesh/shared/health_check_manager.py +313 -0
  25. _mcp_mesh/shared/logging_config.py +190 -7
  26. _mcp_mesh/shared/registry_client_wrapper.py +8 -8
  27. _mcp_mesh/shared/sse_parser.py +19 -17
  28. _mcp_mesh/tracing/execution_tracer.py +26 -1
  29. _mcp_mesh/tracing/fastapi_tracing_middleware.py +3 -4
  30. _mcp_mesh/tracing/trace_context_helper.py +25 -6
  31. {mcp_mesh-0.7.12.dist-info → mcp_mesh-0.7.14.dist-info}/METADATA +1 -1
  32. {mcp_mesh-0.7.12.dist-info → mcp_mesh-0.7.14.dist-info}/RECORD +38 -39
  33. mesh/__init__.py +3 -1
  34. mesh/decorators.py +81 -43
  35. mesh/helpers.py +72 -4
  36. mesh/types.py +48 -4
  37. _mcp_mesh/engine/full_mcp_proxy.py +0 -641
  38. _mcp_mesh/engine/mcp_client_proxy.py +0 -457
  39. _mcp_mesh/shared/health_check_cache.py +0 -246
  40. {mcp_mesh-0.7.12.dist-info → mcp_mesh-0.7.14.dist-info}/WHEEL +0 -0
  41. {mcp_mesh-0.7.12.dist-info → mcp_mesh-0.7.14.dist-info}/licenses/LICENSE +0 -0
@@ -2,12 +2,34 @@
2
2
  Centralized logging configuration for MCP Mesh runtime.
3
3
 
4
4
  This module configures logging based on the MCP_MESH_LOG_LEVEL environment variable.
5
+
6
+ Log Levels:
7
+ CRITICAL (50) - Fatal errors
8
+ ERROR (40) - Errors
9
+ WARNING (30) - Warnings
10
+ INFO (20) - Normal operation (heartbeat counts, connections)
11
+ DEBUG (10) - Debugging info (tool calls, actual issues)
12
+ TRACE (5) - Verbose internals (heartbeat steps, SSE parsing)
5
13
  """
6
14
 
7
15
  import logging
8
16
  import os
9
17
  import sys
10
18
 
19
+ # Define TRACE level (below DEBUG)
20
+ TRACE = 5
21
+ logging.addLevelName(TRACE, "TRACE")
22
+
23
+
24
+ def _trace(self, message, *args, **kwargs):
25
+ """Log a message with TRACE level."""
26
+ if self.isEnabledFor(TRACE):
27
+ self._log(TRACE, message, args, **kwargs)
28
+
29
+
30
+ # Add trace method to Logger class
31
+ logging.Logger.trace = _trace
32
+
11
33
 
12
34
  class SafeStreamHandler(logging.StreamHandler):
13
35
  """A stream handler that gracefully handles closed streams."""
@@ -41,6 +63,7 @@ def configure_logging():
41
63
 
42
64
  # Map string to logging level
43
65
  log_levels = {
66
+ "TRACE": TRACE,
44
67
  "DEBUG": logging.DEBUG,
45
68
  "INFO": logging.INFO,
46
69
  "WARNING": logging.WARNING,
@@ -51,13 +74,16 @@ def configure_logging():
51
74
 
52
75
  log_level = log_levels.get(log_level_str, logging.INFO)
53
76
 
54
- # Check if debug mode is enabled
77
+ # Check if debug mode is enabled (sets DEBUG level)
55
78
  debug_mode = os.environ.get("MCP_MESH_DEBUG_MODE", "").lower() in (
56
79
  "true",
57
80
  "1",
58
81
  "yes",
59
82
  )
60
83
 
84
+ # Check if trace mode is enabled via log level
85
+ trace_mode = log_level_str == "TRACE"
86
+
61
87
  # Clear any existing handlers to avoid conflicts
62
88
  root_logger = logging.getLogger()
63
89
  for handler in root_logger.handlers[:]:
@@ -65,18 +91,32 @@ def configure_logging():
65
91
 
66
92
  # Configure with safe stream handler for background threads
67
93
  handler = SafeStreamHandler(sys.stdout)
68
- handler.setLevel(logging.DEBUG) # Handler allows all, loggers filter
94
+ handler.setLevel(TRACE) # Handler allows all levels including TRACE
69
95
  handler.setFormatter(logging.Formatter("%(levelname)-8s %(message)s"))
70
96
 
71
97
  root_logger.addHandler(handler)
72
98
 
73
99
  # Root logger always INFO - all third-party libs stay quiet
74
100
  # This is the allowlist approach: instead of blocklisting noisy loggers one by one,
75
- # we keep root at INFO and only elevate mcp-mesh loggers to DEBUG
101
+ # we keep root at INFO and only elevate mcp-mesh loggers
76
102
  root_logger.setLevel(logging.INFO)
77
103
 
78
- # Only MCP Mesh loggers get DEBUG when debug mode is on
79
- if debug_mode:
104
+ # Suppress noisy third-party loggers (FastMCP/MCP library logs)
105
+ # These produce verbose INFO logs like "Terminating session: None" and
106
+ # "Processing request of type CallToolRequest" that clutter debug output
107
+ logging.getLogger("mcp").setLevel(logging.WARNING)
108
+ logging.getLogger("mcp.server").setLevel(logging.WARNING)
109
+ logging.getLogger("mcp.client").setLevel(logging.WARNING)
110
+ logging.getLogger("fastmcp").setLevel(logging.WARNING)
111
+
112
+ # Set MCP Mesh logger levels based on configuration
113
+ if trace_mode:
114
+ # TRACE mode: show everything including verbose heartbeat internals
115
+ logging.getLogger("mesh").setLevel(TRACE)
116
+ logging.getLogger("mcp_mesh").setLevel(TRACE)
117
+ logging.getLogger("_mcp_mesh").setLevel(TRACE)
118
+ elif debug_mode:
119
+ # DEBUG mode: show debug info but not verbose trace logs
80
120
  logging.getLogger("mesh").setLevel(logging.DEBUG)
81
121
  logging.getLogger("mcp_mesh").setLevel(logging.DEBUG)
82
122
  logging.getLogger("_mcp_mesh").setLevel(logging.DEBUG)
@@ -86,9 +126,152 @@ def configure_logging():
86
126
  logging.getLogger("mcp_mesh").setLevel(log_level)
87
127
  logging.getLogger("_mcp_mesh").setLevel(log_level)
88
128
 
89
- # Return the configured level for reference (DEBUG if debug mode, else configured level)
90
- return logging.DEBUG if debug_mode else log_level
129
+ # Return the configured level for reference
130
+ if trace_mode:
131
+ return TRACE
132
+ elif debug_mode:
133
+ return logging.DEBUG
134
+ else:
135
+ return log_level
91
136
 
92
137
 
93
138
  # Configure logging on module import
94
139
  _configured_level = configure_logging()
140
+
141
+
142
+ # ============================================================================
143
+ # Log Value Formatting Helpers
144
+ # ============================================================================
145
+
146
+
147
+ def get_trace_prefix() -> str:
148
+ """Get trace ID prefix for log lines if tracing is active.
149
+
150
+ Returns:
151
+ String like "[trace=abc12345] " if trace context exists, empty string otherwise.
152
+ """
153
+ try:
154
+ from ..tracing.context import TraceContext
155
+
156
+ trace_info = TraceContext.get_current()
157
+ if trace_info and trace_info.trace_id:
158
+ # Use first 8 chars for readability
159
+ short_id = trace_info.trace_id[:8]
160
+ return f"[{short_id}] "
161
+ except Exception:
162
+ # Tracing not available or not configured
163
+ pass
164
+ return ""
165
+
166
+
167
+ def format_log_value(value, max_len: int = 1000) -> str:
168
+ """Format a value for logging with truncation.
169
+
170
+ Provides a readable representation of values with size info and truncation
171
+ for large payloads. Suitable for DEBUG level logging.
172
+
173
+ Args:
174
+ value: Any value to format
175
+ max_len: Maximum length before truncation (default 1000)
176
+
177
+ Returns:
178
+ Formatted string representation
179
+ """
180
+ if value is None:
181
+ return "None"
182
+
183
+ type_name = type(value).__name__
184
+
185
+ try:
186
+ if isinstance(value, dict):
187
+ content = str(value)
188
+ if len(content) > max_len:
189
+ return f"{type_name}({len(value)} keys): {content[:max_len]}..."
190
+ return content
191
+
192
+ elif isinstance(value, (list, tuple)):
193
+ content = str(value)
194
+ if len(content) > max_len:
195
+ return f"{type_name}({len(value)} items): {content[:max_len]}..."
196
+ return content
197
+
198
+ elif isinstance(value, str):
199
+ if len(value) > max_len:
200
+ return f'"{value[:max_len]}..." ({len(value)} chars)'
201
+ return f'"{value}"'
202
+
203
+ elif isinstance(value, bytes):
204
+ return f"bytes({len(value)} bytes)"
205
+
206
+ elif hasattr(value, "__dict__"):
207
+ # Object with attributes - show class name and key attributes
208
+ content = str(value)
209
+ if len(content) > max_len:
210
+ return f"{type_name}: {content[:max_len]}..."
211
+ return f"{type_name}: {content}"
212
+
213
+ else:
214
+ content = str(value)
215
+ if len(content) > max_len:
216
+ return f"{type_name}: {content[:max_len]}..."
217
+ return content
218
+
219
+ except Exception as e:
220
+ return f"{type_name}: <error formatting: {e}>"
221
+
222
+
223
+ def format_args_summary(args: tuple, kwargs: dict) -> str:
224
+ """Format function arguments as a summary (keys only).
225
+
226
+ Suitable for concise DEBUG logging showing what was passed.
227
+
228
+ Args:
229
+ args: Positional arguments tuple
230
+ kwargs: Keyword arguments dict
231
+
232
+ Returns:
233
+ Summary string like "args=(2), kwargs=['name', 'value']"
234
+ """
235
+ parts = []
236
+
237
+ if args:
238
+ parts.append(f"args=({len(args)})")
239
+
240
+ if kwargs:
241
+ keys = list(kwargs.keys())
242
+ parts.append(f"kwargs={keys}")
243
+
244
+ return ", ".join(parts) if parts else "no args"
245
+
246
+
247
+ def format_result_summary(result) -> str:
248
+ """Format a result value as a summary (type and size).
249
+
250
+ Suitable for concise DEBUG logging showing what was returned.
251
+
252
+ Args:
253
+ result: The return value
254
+
255
+ Returns:
256
+ Summary string like "dict(3 keys)" or "str(150 chars)"
257
+ """
258
+ if result is None:
259
+ return "None"
260
+
261
+ type_name = type(result).__name__
262
+
263
+ try:
264
+ if isinstance(result, dict):
265
+ return f"dict({len(result)} keys)"
266
+ elif isinstance(result, (list, tuple)):
267
+ return f"{type_name}({len(result)} items)"
268
+ elif isinstance(result, str):
269
+ return f"str({len(result)} chars)"
270
+ elif isinstance(result, bytes):
271
+ return f"bytes({len(result)} bytes)"
272
+ elif isinstance(result, (int, float, bool)):
273
+ return str(result)
274
+ else:
275
+ return type_name
276
+ except Exception:
277
+ return type_name
@@ -72,7 +72,7 @@ class RegistryClientWrapper:
72
72
  )
73
73
 
74
74
  registration_json = json.dumps(registration_dict, indent=2, default=str)
75
- self.logger.debug(
75
+ self.logger.trace(
76
76
  f"🔍 Full heartbeat registration payload:\n{registration_json}"
77
77
  )
78
78
 
@@ -148,7 +148,7 @@ class RegistryClientWrapper:
148
148
  )
149
149
 
150
150
  parsed_dep["kwargs"] = kwargs_data
151
- self.logger.debug(
151
+ self.logger.trace(
152
152
  f"🔧 Parsed kwargs for {dep_resolution.get('capability')}: {kwargs_data}"
153
153
  )
154
154
  except (json.JSONDecodeError, TypeError) as e:
@@ -178,7 +178,7 @@ class RegistryClientWrapper:
178
178
  FastHeartbeatStatus indicating required action
179
179
  """
180
180
  try:
181
- self.logger.debug(
181
+ self.logger.trace(
182
182
  f"🚀 Performing fast heartbeat check for agent '{agent_id}'"
183
183
  )
184
184
 
@@ -189,14 +189,14 @@ class RegistryClientWrapper:
189
189
 
190
190
  # Extract the actual HTTP status code from the response
191
191
  status_code = http_response.status_code
192
- self.logger.debug(
192
+ self.logger.trace(
193
193
  f"Fast heartbeat HEAD request for agent '{agent_id}' returned HTTP {status_code}"
194
194
  )
195
195
 
196
196
  # Convert HTTP status to semantic status
197
197
  status = FastHeartbeatStatusUtil.from_http_code(status_code)
198
198
 
199
- self.logger.debug(
199
+ self.logger.trace(
200
200
  f"✅ Fast heartbeat check completed for agent '{agent_id}': {status.value}"
201
201
  )
202
202
  return status
@@ -214,7 +214,7 @@ class RegistryClientWrapper:
214
214
 
215
215
  # Handle 410 Gone specifically (agent unknown)
216
216
  if "(410)" in error_str or "Gone" in error_str:
217
- self.logger.debug(
217
+ self.logger.trace(
218
218
  f"🔍 Fast heartbeat: Agent '{agent_id}' unknown (410 Gone) - re-registration needed"
219
219
  )
220
220
  return FastHeartbeatStatus.AGENT_UNKNOWN
@@ -423,7 +423,7 @@ class RegistryClientWrapper:
423
423
  "filter_mode": filter_mode,
424
424
  }
425
425
 
426
- self.logger.debug(
426
+ self.logger.trace(
427
427
  f"🤖 Extracted llm_filter for {func_name}: {len(normalized_filter)} filters, mode={filter_mode}"
428
428
  )
429
429
  break
@@ -449,7 +449,7 @@ class RegistryClientWrapper:
449
449
  namespace=provider.get("namespace", "default"),
450
450
  )
451
451
 
452
- self.logger.debug(
452
+ self.logger.trace(
453
453
  f"🔌 Extracted llm_provider for {func_name}: {llm_provider_data.model_dump()}"
454
454
  )
455
455
  break
@@ -34,11 +34,11 @@ class SSEParser:
34
34
  Raises:
35
35
  RuntimeError: If SSE response cannot be parsed
36
36
  """
37
- logger.debug(f"🔧 SSEParser.parse_sse_response called from {context}")
38
- logger.debug(
37
+ logger.trace(f"🔧 SSEParser.parse_sse_response called from {context}")
38
+ logger.trace(
39
39
  f"🔧 Response text length: {len(response_text)}, starts with 'event:': {response_text.startswith('event:')}"
40
40
  )
41
- logger.debug(f"🔧 Response preview: {repr(response_text[:100])}...")
41
+ logger.trace(f"🔧 Response preview: {repr(response_text[:100])}...")
42
42
 
43
43
  # Check if this is SSE format (can be malformed and not start with "event:")
44
44
  is_sse_format = (
@@ -49,13 +49,13 @@ class SSEParser:
49
49
 
50
50
  if not is_sse_format:
51
51
  # Not an SSE response, try parsing as plain JSON
52
- logger.debug(f"🔧 {context}: Parsing as plain JSON (not SSE format)")
53
- logger.debug(
52
+ logger.trace(f"🔧 {context}: Parsing as plain JSON (not SSE format)")
53
+ logger.trace(
54
54
  f"🔧 {context}: Response preview: {repr(response_text[:200])}..."
55
55
  )
56
56
  try:
57
57
  result = json.loads(response_text)
58
- logger.debug(f"🔧 {context}: Plain JSON parsed successfully")
58
+ logger.trace(f"🔧 {context}: Plain JSON parsed successfully")
59
59
  return result
60
60
  except json.JSONDecodeError as e:
61
61
  logger.error(f"🔧 {context}: Plain JSON parse failed: {e}")
@@ -65,7 +65,7 @@ class SSEParser:
65
65
  raise RuntimeError(f"Invalid JSON response in {context}: {e}")
66
66
 
67
67
  # Parse SSE format: find first valid JSON in data lines
68
- logger.debug(f"🔧 {context}: Parsing SSE format - looking for first valid JSON")
68
+ logger.trace(f"🔧 {context}: Parsing SSE format - looking for first valid JSON")
69
69
  data_line_count = 0
70
70
  first_valid_json = None
71
71
 
@@ -79,22 +79,24 @@ class SSEParser:
79
79
  parsed_json = json.loads(data_content)
80
80
  if first_valid_json is None:
81
81
  first_valid_json = parsed_json
82
- logger.debug(f"🔧 {context}: Found first valid JSON in data line {data_line_count}")
82
+ logger.trace(
83
+ f"🔧 {context}: Found first valid JSON in data line {data_line_count}"
84
+ )
83
85
  except json.JSONDecodeError:
84
86
  # Skip invalid JSON lines - this is expected behavior
85
- logger.debug(f"🔧 {context}: Skipping invalid JSON in data line {data_line_count}: {data_content[:50]}...")
87
+ logger.trace(
88
+ f"🔧 {context}: Skipping invalid JSON in data line {data_line_count}: {data_content[:50]}..."
89
+ )
86
90
  continue
87
91
 
88
- logger.debug(
89
- f"🔧 {context}: Processed {data_line_count} data lines"
90
- )
92
+ logger.trace(f"🔧 {context}: Processed {data_line_count} data lines")
91
93
 
92
94
  # Return first valid JSON found
93
95
  if first_valid_json is None:
94
96
  logger.error(f"🔧 {context}: No valid JSON found in SSE response")
95
- raise RuntimeError(f"Could not parse SSE response from FastMCP")
97
+ raise RuntimeError("Could not parse SSE response from FastMCP")
96
98
 
97
- logger.debug(
99
+ logger.trace(
98
100
  f"🔧 {context}: SSE parsing successful! Result type: {type(first_valid_json)}"
99
101
  )
100
102
  return first_valid_json
@@ -152,14 +154,14 @@ class SSEStreamProcessor:
152
154
  Returns:
153
155
  List of complete JSON objects found in this chunk
154
156
  """
155
- self.logger.debug(
157
+ self.logger.trace(
156
158
  f"🌊 SSEStreamProcessor.process_chunk called for {self.context}, chunk size: {len(chunk_bytes)}"
157
159
  )
158
160
 
159
161
  try:
160
162
  chunk_text = chunk_bytes.decode("utf-8")
161
163
  self.buffer += chunk_text
162
- self.logger.debug(
164
+ self.logger.trace(
163
165
  f"🌊 {self.context}: Buffer size after chunk: {len(self.buffer)}"
164
166
  )
165
167
  except UnicodeDecodeError:
@@ -190,7 +192,7 @@ class SSEStreamProcessor:
190
192
  if parsed:
191
193
  results.append(parsed)
192
194
 
193
- self.logger.debug(
195
+ self.logger.trace(
194
196
  f"🌊 {self.context}: Processed {events_processed} complete SSE events, yielding {len(results)} JSON objects"
195
197
  )
196
198
  return results
@@ -27,7 +27,9 @@ class ExecutionTracer:
27
27
  self.function_name = function_name
28
28
  self.logger = logger_instance
29
29
  self.start_time: float | None = None
30
- self.trace_context: Any | None = None
30
+ self.trace_context: Any | None = (
31
+ None # Parent's trace context (to restore after execution)
32
+ )
31
33
  self.execution_metadata: dict = {}
32
34
 
33
35
  def start_execution(
@@ -138,11 +140,34 @@ class ExecutionTracer:
138
140
  # Save execution trace to Redis for distributed tracing storage
139
141
  publish_trace_with_fallback(self.execution_metadata, self.logger)
140
142
 
143
+ # CRITICAL: Restore parent's trace context so sibling calls have correct parent
144
+ # Without this, subsequent calls become children of this span instead of siblings
145
+ self._restore_parent_context()
146
+
141
147
  except Exception as e:
142
148
  self.logger.warning(
143
149
  f"Failed to complete execution logging for {self.function_name}: {e}"
144
150
  )
145
151
 
152
+ def _restore_parent_context(self) -> None:
153
+ """Restore the parent's trace context after this span completes.
154
+
155
+ This ensures sibling function calls share the same parent instead of
156
+ becoming nested children of each other.
157
+ """
158
+ try:
159
+ from .context import TraceContext
160
+
161
+ if self.trace_context:
162
+ # Restore to parent's context (the context that existed before this span)
163
+ TraceContext.set_current(
164
+ trace_id=self.trace_context.trace_id,
165
+ span_id=self.trace_context.span_id,
166
+ parent_span=self.trace_context.parent_span,
167
+ )
168
+ except Exception as e:
169
+ self.logger.debug(f"Failed to restore parent trace context: {e}")
170
+
146
171
  @staticmethod
147
172
  def trace_function_execution(
148
173
  func: Callable,
@@ -40,6 +40,9 @@ class FastAPITracingMiddleware(BaseHTTPMiddleware):
40
40
  if not is_tracing_enabled():
41
41
  return await call_next(request)
42
42
 
43
+ self.logger.debug(
44
+ f"[TRACE] Processing request {request.method} {request.url.path}"
45
+ )
43
46
 
44
47
  # Setup distributed tracing context from request headers
45
48
  try:
@@ -50,7 +53,6 @@ class FastAPITracingMiddleware(BaseHTTPMiddleware):
50
53
  request
51
54
  )
52
55
 
53
-
54
56
  # Setup trace context for this request lifecycle
55
57
  TraceContextHelper.setup_request_trace_context(trace_context, self.logger)
56
58
 
@@ -62,7 +64,6 @@ class FastAPITracingMiddleware(BaseHTTPMiddleware):
62
64
  route_name = self._extract_route_name(request)
63
65
  start_time = time.time()
64
66
 
65
-
66
67
  try:
67
68
  # Process the request
68
69
  response = await call_next(request)
@@ -83,7 +84,6 @@ class FastAPITracingMiddleware(BaseHTTPMiddleware):
83
84
  status_code=response.status_code,
84
85
  )
85
86
 
86
-
87
87
  return response
88
88
 
89
89
  except Exception as e:
@@ -103,7 +103,6 @@ class FastAPITracingMiddleware(BaseHTTPMiddleware):
103
103
  error=str(e),
104
104
  )
105
105
 
106
-
107
106
  raise # Re-raise the original exception
108
107
 
109
108
  def _extract_route_name(self, request: Request) -> str:
@@ -11,6 +11,23 @@ from typing import Any, Optional
11
11
  logger = logging.getLogger(__name__)
12
12
 
13
13
 
14
+ def get_header_case_insensitive(headers_list: list, name: str) -> str:
15
+ """Get header value case-insensitively from ASGI headers list.
16
+
17
+ Args:
18
+ headers_list: List of (key, value) tuples with bytes
19
+ name: Header name to find (case-insensitive)
20
+
21
+ Returns:
22
+ Header value as string, or empty string if not found
23
+ """
24
+ name_lower = name.lower()
25
+ for key, value in headers_list:
26
+ if key.decode().lower() == name_lower:
27
+ return value.decode()
28
+ return ""
29
+
30
+
14
31
  class TraceContextHelper:
15
32
  """Helper class to handle HTTP request trace context setup and distributed tracing."""
16
33
 
@@ -37,16 +54,18 @@ class TraceContextHelper:
37
54
  extracted_trace_id = trace_context.get("trace_id")
38
55
  if extracted_trace_id and extracted_trace_id.strip():
39
56
  # EXISTING TRACE: This service is being called by another service
40
- from .utils import generate_span_id
41
-
42
- current_span_id = generate_span_id()
57
+ # Pass through the incoming parent_span directly - ExecutionTracer will create
58
+ # the actual function span as a child of this parent.
59
+ # This avoids creating an unpublished intermediate middleware span.
43
60
  parent_span_id = trace_context.get("parent_span")
44
61
 
45
- # Set context that will be used throughout request lifecycle
62
+ # Set context with incoming parent_span as the current span
63
+ # When ExecutionTracer runs, it will create a child span with this as parent
46
64
  TraceContext.set_current(
47
65
  trace_id=extracted_trace_id,
48
- span_id=current_span_id,
49
- parent_span=parent_span_id,
66
+ span_id=parent_span_id
67
+ or "", # Use parent as current (will be overwritten by ExecutionTracer)
68
+ parent_span=None, # No grandparent needed at this level
50
69
  )
51
70
  else:
52
71
  # NEW ROOT TRACE: This service is the entry point (no incoming trace headers)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-mesh
3
- Version: 0.7.12
3
+ Version: 0.7.14
4
4
  Summary: Kubernetes-native platform for distributed MCP applications
5
5
  Project-URL: Homepage, https://github.com/dhyansraj/mcp-mesh
6
6
  Project-URL: Documentation, https://github.com/dhyansraj/mcp-mesh/tree/main/docs