mcp-mesh 0.5.0__py3-none-any.whl → 0.5.1__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/__init__.py +12 -1
- _mcp_mesh/engine/async_mcp_client.py +2 -2
- _mcp_mesh/engine/decorator_registry.py +98 -8
- _mcp_mesh/engine/dependency_injector.py +105 -14
- _mcp_mesh/engine/full_mcp_proxy.py +4 -4
- _mcp_mesh/engine/http_wrapper.py +9 -20
- _mcp_mesh/engine/mcp_client_proxy.py +1 -1
- _mcp_mesh/engine/unified_mcp_proxy.py +813 -0
- _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +12 -21
- _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +12 -5
- _mcp_mesh/pipeline/api_startup/api_server_setup.py +95 -20
- _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +19 -183
- _mcp_mesh/tracing/agent_context_helper.py +1 -1
- _mcp_mesh/tracing/execution_tracer.py +41 -0
- {mcp_mesh-0.5.0.dist-info → mcp_mesh-0.5.1.dist-info}/METADATA +1 -1
- {mcp_mesh-0.5.0.dist-info → mcp_mesh-0.5.1.dist-info}/RECORD +20 -19
- mesh/types.py +109 -48
- {mcp_mesh-0.5.0.dist-info → mcp_mesh-0.5.1.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.5.0.dist-info → mcp_mesh-0.5.1.dist-info}/licenses/LICENSE +0 -0
_mcp_mesh/__init__.py
CHANGED
_mcp_mesh/engine/__init__.py
CHANGED
|
@@ -17,12 +17,15 @@ __all__ = [
|
|
|
17
17
|
# Dependency injection
|
|
18
18
|
"DependencyInjector",
|
|
19
19
|
"get_global_injector",
|
|
20
|
-
# MCP client proxies
|
|
20
|
+
# MCP client proxies (legacy)
|
|
21
21
|
"MCPClientProxy",
|
|
22
22
|
"EnhancedMCPClientProxy",
|
|
23
23
|
"FullMCPProxy",
|
|
24
24
|
"EnhancedFullMCPProxy",
|
|
25
25
|
"AsyncMCPClient",
|
|
26
|
+
# Unified MCP proxy (recommended)
|
|
27
|
+
"UnifiedMCPProxy",
|
|
28
|
+
"EnhancedUnifiedMCPProxy",
|
|
26
29
|
# Self-dependency proxy
|
|
27
30
|
"SelfDependencyProxy",
|
|
28
31
|
# Decorator registry
|
|
@@ -76,6 +79,14 @@ def __getattr__(name):
|
|
|
76
79
|
from .async_mcp_client import AsyncMCPClient
|
|
77
80
|
|
|
78
81
|
return AsyncMCPClient
|
|
82
|
+
elif name == "UnifiedMCPProxy":
|
|
83
|
+
from .unified_mcp_proxy import UnifiedMCPProxy
|
|
84
|
+
|
|
85
|
+
return UnifiedMCPProxy
|
|
86
|
+
elif name == "EnhancedUnifiedMCPProxy":
|
|
87
|
+
from .unified_mcp_proxy import EnhancedUnifiedMCPProxy
|
|
88
|
+
|
|
89
|
+
return EnhancedUnifiedMCPProxy
|
|
79
90
|
# Self-dependency proxy
|
|
80
91
|
elif name == "SelfDependencyProxy":
|
|
81
92
|
from .self_dependency_proxy import SelfDependencyProxy
|
|
@@ -39,7 +39,7 @@ class AsyncMCPClient:
|
|
|
39
39
|
|
|
40
40
|
async def _make_request(self, payload: dict) -> dict:
|
|
41
41
|
"""Make async HTTP request to MCP endpoint."""
|
|
42
|
-
url = f"{self.endpoint}/mcp
|
|
42
|
+
url = f"{self.endpoint}/mcp"
|
|
43
43
|
|
|
44
44
|
try:
|
|
45
45
|
# Use httpx for proper async HTTP requests (better threading support than aiohttp)
|
|
@@ -91,7 +91,7 @@ class AsyncMCPClient:
|
|
|
91
91
|
|
|
92
92
|
async def _make_request_sync(self, payload: dict) -> dict:
|
|
93
93
|
"""Fallback sync HTTP request using urllib."""
|
|
94
|
-
url = f"{self.endpoint}/mcp
|
|
94
|
+
url = f"{self.endpoint}/mcp"
|
|
95
95
|
data = json.dumps(payload).encode("utf-8")
|
|
96
96
|
|
|
97
97
|
# Create request
|
|
@@ -277,6 +277,28 @@ class DecoratorRegistry:
|
|
|
277
277
|
# Cache for resolved agent configuration to avoid repeated work
|
|
278
278
|
_cached_agent_config: Optional[dict[str, Any]] = None
|
|
279
279
|
|
|
280
|
+
@classmethod
|
|
281
|
+
def update_agent_config(cls, updates: dict[str, Any]) -> None:
|
|
282
|
+
"""
|
|
283
|
+
Update the cached agent configuration with new values.
|
|
284
|
+
|
|
285
|
+
This is useful for API services that generate their agent ID
|
|
286
|
+
during pipeline execution and need to store it for telemetry.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
updates: Dictionary of config values to update
|
|
290
|
+
"""
|
|
291
|
+
if cls._cached_agent_config is None:
|
|
292
|
+
# Initialize with current resolved config if not cached yet
|
|
293
|
+
cls._cached_agent_config = cls.get_resolved_agent_config().copy()
|
|
294
|
+
|
|
295
|
+
# Update with new values
|
|
296
|
+
cls._cached_agent_config.update(updates)
|
|
297
|
+
|
|
298
|
+
logger.debug(
|
|
299
|
+
f"🔧 Updated cached agent configuration with: {updates}"
|
|
300
|
+
)
|
|
301
|
+
|
|
280
302
|
@classmethod
|
|
281
303
|
def get_resolved_agent_config(cls) -> dict[str, Any]:
|
|
282
304
|
"""
|
|
@@ -288,11 +310,14 @@ class DecoratorRegistry:
|
|
|
288
310
|
Returns:
|
|
289
311
|
dict: Pre-resolved configuration with consistent agent_id
|
|
290
312
|
"""
|
|
291
|
-
#
|
|
292
|
-
if cls._cached_agent_config is not None:
|
|
313
|
+
# Step 1: Check if cached configuration already has agent_id (from API pipeline)
|
|
314
|
+
if cls._cached_agent_config is not None and cls._cached_agent_config.get('agent_id'):
|
|
315
|
+
logger.debug(
|
|
316
|
+
f"🔧 Using cached agent configuration: agent_id='{cls._cached_agent_config.get('agent_id')}'"
|
|
317
|
+
)
|
|
293
318
|
return cls._cached_agent_config
|
|
294
319
|
|
|
295
|
-
# If we have explicit @mesh.agent configuration, use it
|
|
320
|
+
# Step 2: If we have explicit @mesh.agent configuration, use it
|
|
296
321
|
if cls._mesh_agents:
|
|
297
322
|
for agent_name, decorated_func in cls._mesh_agents.items():
|
|
298
323
|
# Return the already-resolved configuration from decorator
|
|
@@ -306,14 +331,23 @@ class DecoratorRegistry:
|
|
|
306
331
|
)
|
|
307
332
|
return resolved_config
|
|
308
333
|
|
|
309
|
-
#
|
|
310
|
-
# This happens when only @mesh.tool decorators are used
|
|
311
|
-
from mesh.decorators import _get_or_create_agent_id
|
|
312
|
-
|
|
334
|
+
# Step 3: Fallback to synthetic defaults when no @mesh.agent decorator exists
|
|
335
|
+
# This happens when only @mesh.tool decorators are used and no cached agent_id
|
|
313
336
|
from ..shared.config_resolver import ValidationRule, get_config_value
|
|
314
337
|
from ..shared.defaults import MeshDefaults
|
|
315
338
|
|
|
316
|
-
|
|
339
|
+
# Check if we're in an API context (have mesh_route decorators)
|
|
340
|
+
mesh_routes = cls.get_all_by_type("mesh_route")
|
|
341
|
+
is_api_context = len(mesh_routes) > 0
|
|
342
|
+
|
|
343
|
+
if is_api_context:
|
|
344
|
+
# Use API service ID generation logic for consistency
|
|
345
|
+
agent_id = cls._generate_api_service_id_fallback()
|
|
346
|
+
else:
|
|
347
|
+
# Use standard MCP agent ID generation
|
|
348
|
+
from mesh.decorators import _get_or_create_agent_id
|
|
349
|
+
agent_id = _get_or_create_agent_id()
|
|
350
|
+
|
|
317
351
|
fallback_config = {
|
|
318
352
|
"name": None,
|
|
319
353
|
"version": get_config_value(
|
|
@@ -368,6 +402,62 @@ class DecoratorRegistry:
|
|
|
368
402
|
)
|
|
369
403
|
return fallback_config
|
|
370
404
|
|
|
405
|
+
@classmethod
|
|
406
|
+
def _generate_api_service_id_fallback(cls) -> str:
|
|
407
|
+
"""
|
|
408
|
+
Generate API service ID as fallback using same priority logic as API pipeline.
|
|
409
|
+
|
|
410
|
+
Priority order:
|
|
411
|
+
1. MCP_MESH_API_NAME environment variable
|
|
412
|
+
2. MCP_MESH_AGENT_NAME environment variable (fallback)
|
|
413
|
+
3. Default to "api-{uuid8}"
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
Generated service ID with UUID suffix matching API service format
|
|
417
|
+
"""
|
|
418
|
+
import uuid
|
|
419
|
+
|
|
420
|
+
from ..shared.config_resolver import ValidationRule, get_config_value
|
|
421
|
+
|
|
422
|
+
# Check for API-specific environment variable first (same as API pipeline)
|
|
423
|
+
api_name = get_config_value(
|
|
424
|
+
"MCP_MESH_API_NAME",
|
|
425
|
+
default=None,
|
|
426
|
+
rule=ValidationRule.STRING_RULE,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Fallback to general agent name env var
|
|
430
|
+
if not api_name:
|
|
431
|
+
api_name = get_config_value(
|
|
432
|
+
"MCP_MESH_AGENT_NAME",
|
|
433
|
+
default=None,
|
|
434
|
+
rule=ValidationRule.STRING_RULE,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Clean the service name if provided
|
|
438
|
+
if api_name:
|
|
439
|
+
cleaned_name = api_name.lower().replace(" ", "-").replace("_", "-")
|
|
440
|
+
cleaned_name = "-".join(part for part in cleaned_name.split("-") if part)
|
|
441
|
+
else:
|
|
442
|
+
cleaned_name = ""
|
|
443
|
+
|
|
444
|
+
# Generate UUID suffix
|
|
445
|
+
uuid_suffix = str(uuid.uuid4())[:8]
|
|
446
|
+
|
|
447
|
+
# Apply same naming logic as API pipeline
|
|
448
|
+
if not cleaned_name:
|
|
449
|
+
# No name provided: default to "api-{uuid8}"
|
|
450
|
+
service_id = f"api-{uuid_suffix}"
|
|
451
|
+
elif "api" in cleaned_name.lower():
|
|
452
|
+
# Name already contains "api": use "{name}-{uuid8}"
|
|
453
|
+
service_id = f"{cleaned_name}-{uuid_suffix}"
|
|
454
|
+
else:
|
|
455
|
+
# Name doesn't contain "api": use "{name}-api-{uuid8}"
|
|
456
|
+
service_id = f"{cleaned_name}-api-{uuid_suffix}"
|
|
457
|
+
|
|
458
|
+
logger.debug(f"Generated fallback API service ID: '{service_id}' from env name: '{api_name}'")
|
|
459
|
+
return service_id
|
|
460
|
+
|
|
371
461
|
@classmethod
|
|
372
462
|
def get_all_agents(cls) -> list[tuple[Any, dict[str, Any]]]:
|
|
373
463
|
"""
|
|
@@ -288,9 +288,45 @@ class DependencyInjector:
|
|
|
288
288
|
f"🔧 No injection positions for {func.__name__}, creating minimal wrapper for tracking"
|
|
289
289
|
)
|
|
290
290
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
291
|
+
# Check if we need async wrapper for minimal case
|
|
292
|
+
if inspect.iscoroutinefunction(func):
|
|
293
|
+
@functools.wraps(func)
|
|
294
|
+
async def minimal_wrapper(*args, **kwargs):
|
|
295
|
+
# Execute with telemetry tracing even for async functions without dependencies
|
|
296
|
+
try:
|
|
297
|
+
from ..tracing.execution_tracer import ExecutionTracer
|
|
298
|
+
|
|
299
|
+
# Use ExecutionTracer for minimal async wrapper (no dependencies)
|
|
300
|
+
return await ExecutionTracer.trace_function_execution_async(
|
|
301
|
+
func, args, kwargs, [], [], 0, wrapper_logger
|
|
302
|
+
)
|
|
303
|
+
except ImportError:
|
|
304
|
+
# Fallback if tracing is unavailable - never fail user function
|
|
305
|
+
wrapper_logger.debug("🔇 Tracing unavailable, executing without telemetry")
|
|
306
|
+
return await func(*args, **kwargs)
|
|
307
|
+
except Exception as e:
|
|
308
|
+
# Never fail user function due to tracing errors
|
|
309
|
+
wrapper_logger.warning(f"⚠️ Telemetry failed, executing without: {e}")
|
|
310
|
+
return await func(*args, **kwargs)
|
|
311
|
+
else:
|
|
312
|
+
@functools.wraps(func)
|
|
313
|
+
def minimal_wrapper(*args, **kwargs):
|
|
314
|
+
# Execute with telemetry tracing even for functions without dependencies
|
|
315
|
+
try:
|
|
316
|
+
from ..tracing.execution_tracer import ExecutionTracer
|
|
317
|
+
|
|
318
|
+
# Use ExecutionTracer for minimal wrapper (no dependencies)
|
|
319
|
+
return ExecutionTracer.trace_original_function(
|
|
320
|
+
func, args, kwargs, wrapper_logger
|
|
321
|
+
)
|
|
322
|
+
except ImportError:
|
|
323
|
+
# Fallback if tracing is unavailable - never fail user function
|
|
324
|
+
wrapper_logger.debug("🔇 Tracing unavailable, executing without telemetry")
|
|
325
|
+
return func(*args, **kwargs)
|
|
326
|
+
except Exception as e:
|
|
327
|
+
# Never fail user function due to tracing errors
|
|
328
|
+
wrapper_logger.warning(f"⚠️ Telemetry failed, executing without: {e}")
|
|
329
|
+
return func(*args, **kwargs)
|
|
294
330
|
|
|
295
331
|
# Add minimal metadata for compatibility
|
|
296
332
|
minimal_wrapper._mesh_injected_deps = {}
|
|
@@ -390,17 +426,51 @@ class DependencyInjector:
|
|
|
390
426
|
f"🔧 DEPENDENCY_WRAPPER: final_kwargs={final_kwargs}"
|
|
391
427
|
)
|
|
392
428
|
|
|
393
|
-
# ===== EXECUTE WITH DEPENDENCY INJECTION =====
|
|
394
|
-
# Call the original function with injected dependencies
|
|
429
|
+
# ===== EXECUTE WITH TELEMETRY AND DEPENDENCY INJECTION =====
|
|
430
|
+
# Call the original function with telemetry tracing and injected dependencies
|
|
395
431
|
original_func = func._mesh_original_func
|
|
396
432
|
|
|
397
|
-
#
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
433
|
+
# Execute with telemetry tracing
|
|
434
|
+
try:
|
|
435
|
+
from ..tracing.execution_tracer import ExecutionTracer
|
|
436
|
+
|
|
437
|
+
# Use ExecutionTracer for comprehensive telemetry
|
|
438
|
+
if inspect.iscoroutinefunction(original_func):
|
|
439
|
+
# For async functions, await the result directly
|
|
440
|
+
result = await ExecutionTracer.trace_function_execution_async(
|
|
441
|
+
original_func,
|
|
442
|
+
args,
|
|
443
|
+
final_kwargs,
|
|
444
|
+
dependencies,
|
|
445
|
+
mesh_positions,
|
|
446
|
+
injected_count,
|
|
447
|
+
wrapper_logger,
|
|
448
|
+
)
|
|
449
|
+
else:
|
|
450
|
+
# For sync functions, call directly
|
|
451
|
+
result = ExecutionTracer.trace_function_execution(
|
|
452
|
+
original_func,
|
|
453
|
+
args,
|
|
454
|
+
final_kwargs,
|
|
455
|
+
dependencies,
|
|
456
|
+
mesh_positions,
|
|
457
|
+
injected_count,
|
|
458
|
+
wrapper_logger,
|
|
459
|
+
)
|
|
460
|
+
except ImportError:
|
|
461
|
+
# Fallback if tracing is unavailable - never fail user function
|
|
462
|
+
wrapper_logger.debug("🔇 Tracing unavailable, executing without telemetry")
|
|
463
|
+
if inspect.iscoroutinefunction(original_func):
|
|
464
|
+
result = await original_func(*args, **final_kwargs)
|
|
465
|
+
else:
|
|
466
|
+
result = original_func(*args, **final_kwargs)
|
|
467
|
+
except Exception as e:
|
|
468
|
+
# Never fail user function due to tracing errors
|
|
469
|
+
wrapper_logger.warning(f"⚠️ Telemetry failed, executing without: {e}")
|
|
470
|
+
if inspect.iscoroutinefunction(original_func):
|
|
471
|
+
result = await original_func(*args, **final_kwargs)
|
|
472
|
+
else:
|
|
473
|
+
result = original_func(*args, **final_kwargs)
|
|
404
474
|
|
|
405
475
|
wrapper_logger.debug(
|
|
406
476
|
f"🔧 DEPENDENCY_WRAPPER: Original returned: {type(result)}"
|
|
@@ -454,8 +524,29 @@ class DependencyInjector:
|
|
|
454
524
|
final_kwargs[param_name] = dependency
|
|
455
525
|
injected_count += 1
|
|
456
526
|
|
|
457
|
-
#
|
|
458
|
-
|
|
527
|
+
# ===== EXECUTE WITH TELEMETRY AND DEPENDENCY INJECTION =====
|
|
528
|
+
# Call the original function with telemetry tracing and injected dependencies
|
|
529
|
+
try:
|
|
530
|
+
from ..tracing.execution_tracer import ExecutionTracer
|
|
531
|
+
|
|
532
|
+
# Use ExecutionTracer for comprehensive telemetry
|
|
533
|
+
return ExecutionTracer.trace_function_execution(
|
|
534
|
+
func._mesh_original_func,
|
|
535
|
+
args,
|
|
536
|
+
final_kwargs,
|
|
537
|
+
dependencies,
|
|
538
|
+
mesh_positions,
|
|
539
|
+
injected_count,
|
|
540
|
+
wrapper_logger,
|
|
541
|
+
)
|
|
542
|
+
except ImportError:
|
|
543
|
+
# Fallback if tracing is unavailable - never fail user function
|
|
544
|
+
wrapper_logger.debug("🔇 Tracing unavailable, executing without telemetry")
|
|
545
|
+
return func._mesh_original_func(*args, **final_kwargs)
|
|
546
|
+
except Exception as e:
|
|
547
|
+
# Never fail user function due to tracing errors
|
|
548
|
+
wrapper_logger.warning(f"⚠️ Telemetry failed, executing without: {e}")
|
|
549
|
+
return func._mesh_original_func(*args, **final_kwargs)
|
|
459
550
|
|
|
460
551
|
# Store dependency state on wrapper
|
|
461
552
|
dependency_wrapper._mesh_injected_deps = {}
|
|
@@ -85,7 +85,7 @@ class FullMCPProxy(MCPClientProxy):
|
|
|
85
85
|
try:
|
|
86
86
|
import httpx
|
|
87
87
|
|
|
88
|
-
url = f"{self.endpoint}/mcp
|
|
88
|
+
url = f"{self.endpoint}/mcp"
|
|
89
89
|
|
|
90
90
|
# Build headers with trace context
|
|
91
91
|
headers = {
|
|
@@ -234,7 +234,7 @@ class FullMCPProxy(MCPClientProxy):
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
# URL for MCP protocol endpoint
|
|
237
|
-
url = f"{self.endpoint.rstrip('/')}/mcp
|
|
237
|
+
url = f"{self.endpoint.rstrip('/')}/mcp"
|
|
238
238
|
|
|
239
239
|
# Add session ID to headers for session routing
|
|
240
240
|
headers = {
|
|
@@ -500,7 +500,7 @@ class EnhancedFullMCPProxy(FullMCPProxy):
|
|
|
500
500
|
# Inject trace context headers
|
|
501
501
|
headers = self._inject_trace_headers(headers)
|
|
502
502
|
|
|
503
|
-
url = f"{self.endpoint}/mcp
|
|
503
|
+
url = f"{self.endpoint}/mcp"
|
|
504
504
|
|
|
505
505
|
try:
|
|
506
506
|
import httpx
|
|
@@ -580,7 +580,7 @@ class EnhancedFullMCPProxy(FullMCPProxy):
|
|
|
580
580
|
# Inject trace context headers
|
|
581
581
|
headers = self._inject_trace_headers(headers)
|
|
582
582
|
|
|
583
|
-
url = f"{self.endpoint}/mcp
|
|
583
|
+
url = f"{self.endpoint}/mcp"
|
|
584
584
|
|
|
585
585
|
try:
|
|
586
586
|
import httpx
|
_mcp_mesh/engine/http_wrapper.py
CHANGED
|
@@ -395,6 +395,12 @@ class HttpMcpWrapper:
|
|
|
395
395
|
from starlette.responses import Response
|
|
396
396
|
|
|
397
397
|
class MCPSessionRoutingMiddleware(BaseHTTPMiddleware):
|
|
398
|
+
"""Session routing middleware for MCP requests.
|
|
399
|
+
|
|
400
|
+
Handles session affinity by routing requests to appropriate pods
|
|
401
|
+
based on session ID. Telemetry/tracing is now handled in the
|
|
402
|
+
DI function wrapper for unified coverage of both MCP and API calls.
|
|
403
|
+
"""
|
|
398
404
|
def __init__(self, app, http_wrapper):
|
|
399
405
|
super().__init__(app)
|
|
400
406
|
self.http_wrapper = http_wrapper
|
|
@@ -402,26 +408,9 @@ class HttpMcpWrapper:
|
|
|
402
408
|
|
|
403
409
|
async def dispatch(self, request: Request, call_next):
|
|
404
410
|
# Only handle MCP requests (FastMCP app already only handles /mcp)
|
|
405
|
-
|
|
406
|
-
#
|
|
407
|
-
|
|
408
|
-
from ..tracing.trace_context_helper import TraceContextHelper
|
|
409
|
-
|
|
410
|
-
# Use helper class for trace context extraction and setup
|
|
411
|
-
trace_context = (
|
|
412
|
-
await TraceContextHelper.extract_trace_context_from_request(
|
|
413
|
-
request
|
|
414
|
-
)
|
|
415
|
-
)
|
|
416
|
-
TraceContextHelper.setup_request_trace_context(
|
|
417
|
-
trace_context, self.logger
|
|
418
|
-
)
|
|
419
|
-
except Exception as e:
|
|
420
|
-
import logging
|
|
421
|
-
|
|
422
|
-
logger = logging.getLogger(__name__)
|
|
423
|
-
logger.warning(f"Failed to set trace context: {e}")
|
|
424
|
-
pass
|
|
411
|
+
|
|
412
|
+
# Note: Telemetry/tracing now handled in DI function wrapper for unified approach
|
|
413
|
+
# This middleware focuses purely on session routing
|
|
425
414
|
|
|
426
415
|
# Extract session ID from request
|
|
427
416
|
session_id = await self.http_wrapper._extract_session_id(request)
|
|
@@ -93,7 +93,7 @@ class MCPClientProxy:
|
|
|
93
93
|
"params": {"name": self.function_name, "arguments": kwargs},
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
url = f"{self.endpoint}/mcp
|
|
96
|
+
url = f"{self.endpoint}/mcp" # Remove trailing slash to avoid 307 redirect
|
|
97
97
|
data = json.dumps(payload).encode("utf-8")
|
|
98
98
|
|
|
99
99
|
# Build headers with trace context injection
|