mcp-mesh 0.8.0__py3-none-any.whl → 0.8.0b2__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.
mesh/decorators.py CHANGED
@@ -26,25 +26,6 @@ _runtime_processor: Any | None = None
26
26
  _SHARED_AGENT_ID: str | None = None
27
27
 
28
28
 
29
- def _find_available_port() -> int:
30
- """
31
- Find an available port by binding to port 0 and getting the OS-assigned port.
32
-
33
- This is used when http_port=0 is specified to auto-assign a port.
34
- Works reliably on all platforms (macOS, Linux, Windows) without external tools.
35
-
36
- Returns:
37
- int: An available port number
38
- """
39
- import socket
40
-
41
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
42
- s.bind(("127.0.0.1", 0))
43
- s.listen(1)
44
- port = s.getsockname()[1]
45
- return port
46
-
47
-
48
29
  def _start_uvicorn_immediately(http_host: str, http_port: int):
49
30
  """
50
31
  Start basic uvicorn server immediately to prevent Python interpreter shutdown.
@@ -95,75 +76,8 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
95
76
  app = FastAPI(title="MCP Mesh Agent (Starting)")
96
77
  logger.debug("📦 IMMEDIATE UVICORN: Created minimal FastAPI app")
97
78
 
98
- # Add middleware to strip trace arguments from tool calls BEFORE app starts
99
- # This must be done unconditionally because meshctl --trace sends trace args
100
- # regardless of agent's tracing configuration
101
- try:
102
- import json as json_module
103
-
104
- class TraceArgumentStripperMiddleware:
105
- """Pure ASGI middleware to strip trace arguments from tool calls.
106
-
107
- This middleware ALWAYS runs to strip _trace_id and _parent_span from
108
- MCP tool arguments, preventing Pydantic validation errors when
109
- meshctl --trace is used with agents that don't have tracing enabled.
110
- """
111
-
112
- def __init__(self, app):
113
- self.app = app
114
-
115
- async def __call__(self, scope, receive, send):
116
- if scope["type"] != "http":
117
- await self.app(scope, receive, send)
118
- return
119
-
120
- async def receive_with_trace_stripping():
121
- message = await receive()
122
- if message["type"] == "http.request":
123
- body = message.get("body", b"")
124
- if body:
125
- try:
126
- payload = json_module.loads(body.decode("utf-8"))
127
- if payload.get("method") == "tools/call":
128
- arguments = payload.get("params", {}).get(
129
- "arguments", {}
130
- )
131
- # Strip trace context fields from arguments
132
- if (
133
- "_trace_id" in arguments
134
- or "_parent_span" in arguments
135
- ):
136
- arguments.pop("_trace_id", None)
137
- arguments.pop("_parent_span", None)
138
- modified_body = json_module.dumps(
139
- payload
140
- ).encode("utf-8")
141
- logger.debug(
142
- "[TRACE] Stripped trace fields from arguments"
143
- )
144
- return {
145
- **message,
146
- "body": modified_body,
147
- }
148
- except Exception as e:
149
- logger.debug(
150
- f"[TRACE] Failed to process body for stripping: {e}"
151
- )
152
- return message
153
-
154
- await self.app(scope, receive_with_trace_stripping, send)
155
-
156
- app.add_middleware(TraceArgumentStripperMiddleware)
157
- logger.debug(
158
- "📦 IMMEDIATE UVICORN: Added trace argument stripper middleware"
159
- )
160
- except Exception as e:
161
- logger.warning(
162
- f"⚠️ IMMEDIATE UVICORN: Failed to add trace argument stripper middleware: {e}"
163
- )
164
-
165
- # Add trace context middleware for distributed tracing (optional)
166
- # This handles trace propagation and header injection when tracing is enabled
79
+ # Add trace context middleware for distributed tracing BEFORE app starts
80
+ # This must be done before uvicorn.run() since middleware can't be added after start
167
81
  try:
168
82
  import os
169
83
 
@@ -238,11 +152,11 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
238
152
  except Exception as e:
239
153
  logger.warning(f"Failed to set trace context: {e}")
240
154
 
241
- # Create receive wrapper to extract trace context from arguments
242
- # Note: Argument stripping is handled by TraceArgumentStripperMiddleware
155
+ # Create body-modifying receive wrapper to strip trace fields from arguments
156
+ # This handles the case where trace context is passed in JSON-RPC arguments
243
157
  import json as json_module
244
158
 
245
- async def receive_with_trace_extraction():
159
+ async def receive_with_trace_stripping():
246
160
  message = await receive()
247
161
  if message["type"] == "http.request":
248
162
  body = message.get("body", b"")
@@ -293,14 +207,29 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
293
207
  parent_span = (
294
208
  current_trace.parent_span
295
209
  )
296
- logger.debug(
297
- f"[TRACE] Extracted trace context from arguments: trace_id={arg_trace_id}"
298
- )
299
210
  except Exception:
300
211
  pass
212
+
213
+ # Strip trace context fields from arguments
214
+ if (
215
+ "_trace_id" in arguments
216
+ or "_parent_span" in arguments
217
+ ):
218
+ arguments.pop("_trace_id", None)
219
+ arguments.pop("_parent_span", None)
220
+ modified_body = json_module.dumps(
221
+ payload
222
+ ).encode("utf-8")
223
+ logger.debug(
224
+ "[TRACE] Stripped trace fields from arguments"
225
+ )
226
+ return {
227
+ **message,
228
+ "body": modified_body,
229
+ }
301
230
  except Exception as e:
302
231
  logger.debug(
303
- f"[TRACE] Failed to process body for extraction: {e}"
232
+ f"[TRACE] Failed to process body: {e}"
304
233
  )
305
234
  return message
306
235
 
@@ -320,7 +249,7 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
320
249
  await send(message)
321
250
 
322
251
  await self.app(
323
- scope, receive_with_trace_extraction, send_with_trace_headers
252
+ scope, receive_with_trace_stripping, send_with_trace_headers
324
253
  )
325
254
 
326
255
  app.add_middleware(TraceContextMiddleware)
@@ -370,19 +299,8 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
370
299
 
371
300
  logger.debug("📦 IMMEDIATE UVICORN: Added status endpoints")
372
301
 
373
- # Port handling:
374
- # - http_port=0 explicitly means auto-assign (let uvicorn choose)
375
- # - http_port>0 means use that specific port
376
- # Note: The default is 8080 only if http_port was never specified,
377
- # which is handled upstream in the @mesh.agent decorator
378
- port = http_port
379
-
380
- # Handle http_port=0: find an available port BEFORE starting uvicorn
381
- # This is more reliable than detecting the port after uvicorn starts
382
- # and works on all platforms (Linux containers don't have lsof installed)
383
- if port == 0:
384
- port = _find_available_port()
385
- logger.info(f"🎯 IMMEDIATE UVICORN: Auto-assigned port {port} for agent")
302
+ # Determine port (0 means auto-assign)
303
+ port = http_port if http_port > 0 else 8080
386
304
 
387
305
  logger.debug(
388
306
  f"🚀 IMMEDIATE UVICORN: Starting uvicorn server on {http_host}:{port}"
@@ -618,25 +536,12 @@ def tool(
618
536
  raise ValueError("dependency capability must be a string")
619
537
 
620
538
  # Validate optional dependency fields
621
- # Tags can be strings or arrays of strings (OR alternatives)
622
- # e.g., ["required", ["python", "typescript"]] = required AND (python OR typescript)
623
539
  dep_tags = dep.get("tags", [])
624
540
  if not isinstance(dep_tags, list):
625
541
  raise ValueError("dependency tags must be a list")
626
542
  for tag in dep_tags:
627
- if isinstance(tag, str):
628
- continue # Simple tag - OK
629
- elif isinstance(tag, list):
630
- # OR alternative - validate inner tags are all strings
631
- for inner_tag in tag:
632
- if not isinstance(inner_tag, str):
633
- raise ValueError(
634
- "OR alternative tags must be strings"
635
- )
636
- else:
637
- raise ValueError(
638
- "tags must be strings or arrays of strings (OR alternatives)"
639
- )
543
+ if not isinstance(tag, str):
544
+ raise ValueError("all dependency tags must be strings")
640
545
 
641
546
  dep_version = dep.get("version")
642
547
  if dep_version is not None and not isinstance(dep_version, str):
@@ -754,7 +659,7 @@ def agent(
754
659
  http_port: int = 0,
755
660
  enable_http: bool = True,
756
661
  namespace: str = "default",
757
- heartbeat_interval: int = 5,
662
+ health_interval: int = 5, # Will be overridden by centralized defaults
758
663
  health_check: Callable[[], Awaitable[Any]] | None = None,
759
664
  health_check_ttl: int = 15,
760
665
  auto_run: bool = True, # Changed to True by default!
@@ -779,7 +684,7 @@ def agent(
779
684
  Environment variable: MCP_MESH_HTTP_ENABLED (takes precedence)
780
685
  namespace: Agent namespace (default: "default")
781
686
  Environment variable: MCP_MESH_NAMESPACE (takes precedence)
782
- heartbeat_interval: Heartbeat interval in seconds (default: 5)
687
+ health_interval: Health check interval in seconds (default: 30)
783
688
  Environment variable: MCP_MESH_HEALTH_INTERVAL (takes precedence)
784
689
  health_check: Optional async function that returns HealthStatus
785
690
  Called before heartbeat and on /health endpoint with TTL caching
@@ -796,7 +701,7 @@ def agent(
796
701
  MCP_MESH_HTTP_PORT: Override http_port parameter (integer, 0-65535)
797
702
  MCP_MESH_HTTP_ENABLED: Override enable_http parameter (boolean: true/false)
798
703
  MCP_MESH_NAMESPACE: Override namespace parameter (string)
799
- MCP_MESH_HEALTH_INTERVAL: Override heartbeat_interval parameter (integer, ≥1)
704
+ MCP_MESH_HEALTH_INTERVAL: Override health_interval parameter (integer, ≥1)
800
705
  MCP_MESH_AUTO_RUN: Override auto_run parameter (boolean: true/false)
801
706
  MCP_MESH_AUTO_RUN_INTERVAL: Override auto_run_interval parameter (integer, ≥1)
802
707
 
@@ -847,10 +752,10 @@ def agent(
847
752
  if not isinstance(namespace, str):
848
753
  raise ValueError("namespace must be a string")
849
754
 
850
- if not isinstance(heartbeat_interval, int):
851
- raise ValueError("heartbeat_interval must be an integer")
852
- if heartbeat_interval < 1:
853
- raise ValueError("heartbeat_interval must be at least 1 second")
755
+ if not isinstance(health_interval, int):
756
+ raise ValueError("health_interval must be an integer")
757
+ if health_interval < 1:
758
+ raise ValueError("health_interval must be at least 1 second")
854
759
 
855
760
  if not isinstance(auto_run, bool):
856
761
  raise ValueError("auto_run must be a boolean")
@@ -905,9 +810,9 @@ def agent(
905
810
  # Import centralized defaults
906
811
  from _mcp_mesh.shared.defaults import MeshDefaults
907
812
 
908
- final_heartbeat_interval = get_config_value(
813
+ final_health_interval = get_config_value(
909
814
  "MCP_MESH_HEALTH_INTERVAL",
910
- override=heartbeat_interval,
815
+ override=health_interval,
911
816
  default=MeshDefaults.HEALTH_INTERVAL,
912
817
  rule=ValidationRule.NONZERO_RULE,
913
818
  )
@@ -938,7 +843,7 @@ def agent(
938
843
  "http_port": final_http_port,
939
844
  "enable_http": final_enable_http,
940
845
  "namespace": final_namespace,
941
- "heartbeat_interval": final_heartbeat_interval,
846
+ "health_interval": final_health_interval,
942
847
  "health_check": health_check,
943
848
  "health_check_ttl": health_check_ttl,
944
849
  "auto_run": final_auto_run,
@@ -1077,10 +982,10 @@ def route(
1077
982
  async def upload_resume(
1078
983
  request: Request,
1079
984
  file: UploadFile = File(...),
1080
- pdf_tool: mesh.McpMeshTool = None, # Injected by MCP Mesh
1081
- user_service: mesh.McpMeshTool = None # Injected by MCP Mesh
985
+ pdf_agent: mesh.McpMeshAgent = None, # Injected by MCP Mesh
986
+ user_service: mesh.McpMeshAgent = None # Injected by MCP Mesh
1082
987
  ):
1083
- result = await pdf_tool.extract_text_from_pdf(file)
988
+ result = await pdf_agent.extract_text_from_pdf(file)
1084
989
  await user_service.update_profile(user_data, result)
1085
990
  return {"success": True}
1086
991
  """
@@ -1109,25 +1014,12 @@ def route(
1109
1014
  raise ValueError("dependency capability must be a string")
1110
1015
 
1111
1016
  # Validate optional dependency fields
1112
- # Tags can be strings or arrays of strings (OR alternatives)
1113
- # e.g., ["required", ["python", "typescript"]] = required AND (python OR typescript)
1114
1017
  dep_tags = dep.get("tags", [])
1115
1018
  if not isinstance(dep_tags, list):
1116
1019
  raise ValueError("dependency tags must be a list")
1117
1020
  for tag in dep_tags:
1118
- if isinstance(tag, str):
1119
- continue # Simple tag - OK
1120
- elif isinstance(tag, list):
1121
- # OR alternative - validate inner tags are all strings
1122
- for inner_tag in tag:
1123
- if not isinstance(inner_tag, str):
1124
- raise ValueError(
1125
- "OR alternative tags must be strings"
1126
- )
1127
- else:
1128
- raise ValueError(
1129
- "tags must be strings or arrays of strings (OR alternatives)"
1130
- )
1021
+ if not isinstance(tag, str):
1022
+ raise ValueError("all dependency tags must be strings")
1131
1023
 
1132
1024
  dep_version = dep.get("version")
1133
1025
  if dep_version is not None and not isinstance(dep_version, str):
@@ -1413,34 +1305,6 @@ def llm(
1413
1305
  rule=ValidationRule.STRING_RULE,
1414
1306
  )
1415
1307
 
1416
- # Resolve model with env var override
1417
- resolved_model = get_config_value(
1418
- "MESH_LLM_MODEL",
1419
- override=model,
1420
- default=None,
1421
- rule=ValidationRule.STRING_RULE,
1422
- )
1423
-
1424
- # Warn about missing configuration parameters
1425
- if not system_prompt and not system_prompt_file:
1426
- logger.warning(
1427
- f"⚠️ @mesh.llm: No 'system_prompt' specified for function '{func.__name__}'. "
1428
- f"Using default: 'You are a helpful assistant.' "
1429
- f"Consider adding a custom system_prompt for better results."
1430
- )
1431
-
1432
- if isinstance(provider, str) and provider == "claude" and not resolved_model:
1433
- logger.warning(
1434
- f"⚠️ @mesh.llm: No 'model' specified for function '{func.__name__}'. "
1435
- f"The LLM provider will use its default model. "
1436
- f"Consider specifying a model explicitly (e.g., model='anthropic/claude-sonnet-4-5')."
1437
- )
1438
-
1439
- # Use default system prompt if not provided
1440
- effective_system_prompt = (
1441
- system_prompt if system_prompt else "You are a helpful assistant."
1442
- )
1443
-
1444
1308
  resolved_config = {
1445
1309
  "filter": filter,
1446
1310
  "filter_mode": get_config_value(
@@ -1450,7 +1314,12 @@ def llm(
1450
1314
  rule=ValidationRule.STRING_RULE,
1451
1315
  ),
1452
1316
  "provider": resolved_provider,
1453
- "model": resolved_model,
1317
+ "model": get_config_value(
1318
+ "MESH_LLM_MODEL",
1319
+ override=model,
1320
+ default=None,
1321
+ rule=ValidationRule.STRING_RULE,
1322
+ ),
1454
1323
  "api_key": api_key, # Will be resolved from provider-specific env vars later
1455
1324
  "max_iterations": get_config_value(
1456
1325
  "MESH_LLM_MAX_ITERATIONS",
@@ -1458,7 +1327,7 @@ def llm(
1458
1327
  default=10,
1459
1328
  rule=ValidationRule.NONZERO_RULE,
1460
1329
  ),
1461
- "system_prompt": effective_system_prompt,
1330
+ "system_prompt": system_prompt,
1462
1331
  "system_prompt_file": system_prompt_file,
1463
1332
  # Phase 1: Template metadata
1464
1333
  "is_template": is_template,
mesh/helpers.py CHANGED
@@ -214,58 +214,6 @@ def llm_provider(
214
214
  f"(requested by consumer)"
215
215
  )
216
216
 
217
- # Issue #459: Handle output_schema for vendor-specific structured output
218
- # Convert to response_format for vendors that support it
219
- output_schema = model_params_copy.pop("output_schema", None)
220
- output_type_name = model_params_copy.pop("output_type_name", None)
221
-
222
- # Vendors that support structured output via response_format
223
- supported_structured_output_vendors = (
224
- "openai",
225
- "azure", # Azure OpenAI uses same format as OpenAI
226
- "gemini",
227
- "vertex_ai", # Vertex AI Gemini uses same format as Gemini
228
- "anthropic",
229
- )
230
-
231
- if output_schema:
232
- if vendor in supported_structured_output_vendors:
233
- # Apply vendor-specific response_format for structured output
234
- from _mcp_mesh.engine.provider_handlers import make_schema_strict
235
-
236
- if vendor == "anthropic":
237
- # Claude: doesn't require all properties in 'required', uses strict=False
238
- schema = make_schema_strict(
239
- output_schema, add_all_required=False
240
- )
241
- strict_mode = False
242
- else:
243
- # OpenAI/Azure/Gemini/Vertex: require all properties in 'required', uses strict=True
244
- schema = make_schema_strict(
245
- output_schema, add_all_required=True
246
- )
247
- strict_mode = True
248
-
249
- model_params_copy["response_format"] = {
250
- "type": "json_schema",
251
- "json_schema": {
252
- "name": output_type_name or "Response",
253
- "schema": schema,
254
- "strict": strict_mode,
255
- },
256
- }
257
- logger.debug(
258
- f"🎯 Applied {vendor} response_format for structured output: "
259
- f"{output_type_name} (strict={strict_mode})"
260
- )
261
- else:
262
- # Vendor doesn't support structured output - warn user
263
- logger.warning(
264
- f"⚠️ Structured output schema '{output_type_name or 'Response'}' "
265
- f"was provided but vendor '{vendor}' does not support response_format. "
266
- f"The schema will be ignored and the LLM may return unstructured output."
267
- )
268
-
269
217
  # Build litellm.completion arguments
270
218
  completion_args: dict[str, Any] = {
271
219
  "model": effective_model,
mesh/types.py CHANGED
@@ -2,7 +2,6 @@
2
2
  MCP Mesh type definitions for dependency injection.
3
3
  """
4
4
 
5
- import warnings
6
5
  from collections.abc import AsyncIterator
7
6
  from dataclasses import dataclass
8
7
  from typing import Any, Dict, List, Optional, Protocol
@@ -15,24 +14,24 @@ except ImportError:
15
14
  PYDANTIC_AVAILABLE = False
16
15
 
17
16
 
18
- class McpMeshTool(Protocol):
17
+ class McpMeshAgent(Protocol):
19
18
  """
20
- MCP Mesh tool proxy for dependency injection.
19
+ Unified MCP Mesh agent proxy using FastMCP's built-in client.
21
20
 
22
- This protocol defines the interface for injected tool dependencies. When you declare
23
- a dependency on a remote tool, MCP Mesh injects a proxy that implements this interface.
21
+ This protocol now provides all MCP protocol features using FastMCP's superior client
22
+ implementation, replacing both the old basic and advanced proxy types.
24
23
 
25
24
  Features:
26
- - Simple callable interface for tool invocation
27
- - Full MCP protocol methods (tools, resources, prompts)
25
+ - All MCP protocol methods (tools, resources, prompts)
28
26
  - Streaming support with FastMCP's StreamableHttpTransport
29
27
  - Session management with notifications
30
- - Automatic redirect handling
28
+ - Automatic redirect handling (fixes /mcp/ → /mcp issue)
31
29
  - CallToolResult objects with structured content parsing
30
+ - Enhanced proxy configuration via kwargs
32
31
 
33
32
  Usage Examples:
34
33
  @mesh.tool(dependencies=["date-service"])
35
- def greet(name: str, date_service: McpMeshTool) -> str:
34
+ def greet(name: str, date_service: McpMeshAgent) -> str:
36
35
  # Simple call - proxy knows which remote function to invoke
37
36
  current_date = date_service()
38
37
 
@@ -45,7 +44,7 @@ class McpMeshTool(Protocol):
45
44
  return f"Hello {name}, today is {current_date}"
46
45
 
47
46
  @mesh.tool(dependencies=["file-service"])
48
- async def process_files(file_service: McpMeshTool) -> str:
47
+ async def process_files(file_service: McpMeshAgent) -> str:
49
48
  # Full MCP Protocol usage
50
49
  tools = await file_service.list_tools()
51
50
  resources = await file_service.list_resources()
@@ -63,7 +62,7 @@ class McpMeshTool(Protocol):
63
62
 
64
63
  return "Processing complete"
65
64
 
66
- The proxy provides all MCP protocol features while maintaining a simple callable interface.
65
+ The unified proxy provides all MCP protocol features while maintaining simple callable interface.
67
66
  """
68
67
 
69
68
  def __call__(self, arguments: Optional[dict[str, Any]] = None) -> Any:
@@ -157,15 +156,15 @@ class McpMeshTool(Protocol):
157
156
  handler: Any,
158
157
  ) -> core_schema.CoreSchema:
159
158
  """
160
- Custom Pydantic core schema for McpMeshTool.
159
+ Custom Pydantic core schema for McpMeshAgent.
161
160
 
162
- This makes McpMeshTool parameters appear as optional/nullable in MCP schemas,
161
+ This makes McpMeshAgent parameters appear as optional/nullable in MCP schemas,
163
162
  preventing serialization errors while maintaining type safety for dependency injection.
164
163
 
165
164
  The dependency injection system will replace None values with actual proxy objects
166
165
  at runtime, so MCP callers never need to provide these parameters.
167
166
  """
168
- # Treat McpMeshTool as an optional Any type for MCP serialization
167
+ # Treat McpMeshAgent as an optional Any type for MCP serialization
169
168
  return core_schema.with_default_schema(
170
169
  core_schema.nullable_schema(core_schema.any_schema()),
171
170
  default=None,
@@ -182,32 +181,6 @@ class McpMeshTool(Protocol):
182
181
  }
183
182
 
184
183
 
185
- def _create_deprecated_mcpmeshagent():
186
- """Create McpMeshAgent as a deprecated alias for McpMeshTool."""
187
-
188
- class McpMeshAgent(McpMeshTool, Protocol):
189
- """
190
- Deprecated: Use McpMeshTool instead.
191
-
192
- This is a backwards-compatible alias that will be removed in a future version.
193
- """
194
-
195
- def __init_subclass__(cls, **kwargs):
196
- warnings.warn(
197
- "McpMeshAgent is deprecated, use McpMeshTool instead. "
198
- "McpMeshAgent will be removed in a future version.",
199
- DeprecationWarning,
200
- stacklevel=2,
201
- )
202
- super().__init_subclass__(**kwargs)
203
-
204
- return McpMeshAgent
205
-
206
-
207
- # Deprecated alias for backwards compatibility
208
- McpMeshAgent = _create_deprecated_mcpmeshagent()
209
-
210
-
211
184
  class MeshLlmAgent(Protocol):
212
185
  """
213
186
  LLM agent proxy with automatic agentic loop.