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.
- _mcp_mesh/__init__.py +1 -1
- _mcp_mesh/engine/dependency_injector.py +4 -6
- _mcp_mesh/engine/http_wrapper.py +69 -10
- _mcp_mesh/engine/mesh_llm_agent.py +4 -7
- _mcp_mesh/engine/mesh_llm_agent_injector.py +2 -1
- _mcp_mesh/engine/provider_handlers/__init__.py +14 -1
- _mcp_mesh/engine/provider_handlers/base_provider_handler.py +114 -8
- _mcp_mesh/engine/provider_handlers/claude_handler.py +15 -57
- _mcp_mesh/engine/provider_handlers/gemini_handler.py +181 -0
- _mcp_mesh/engine/provider_handlers/openai_handler.py +8 -63
- _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +16 -10
- _mcp_mesh/engine/response_parser.py +61 -15
- _mcp_mesh/engine/unified_mcp_proxy.py +18 -34
- _mcp_mesh/pipeline/__init__.py +9 -20
- _mcp_mesh/pipeline/api_heartbeat/__init__.py +12 -7
- _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +23 -49
- _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +425 -0
- _mcp_mesh/pipeline/api_startup/api_pipeline.py +7 -9
- _mcp_mesh/pipeline/api_startup/api_server_setup.py +91 -70
- _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +22 -23
- _mcp_mesh/pipeline/api_startup/middleware_integration.py +32 -24
- _mcp_mesh/pipeline/api_startup/route_collection.py +2 -4
- _mcp_mesh/pipeline/mcp_heartbeat/__init__.py +5 -17
- _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +695 -0
- _mcp_mesh/pipeline/mcp_startup/__init__.py +2 -5
- _mcp_mesh/pipeline/mcp_startup/configuration.py +1 -1
- _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +5 -6
- _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
- _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +21 -9
- _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +3 -8
- _mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -2
- _mcp_mesh/reload.py +1 -3
- _mcp_mesh/shared/__init__.py +2 -8
- _mcp_mesh/shared/config_resolver.py +124 -80
- _mcp_mesh/shared/defaults.py +89 -14
- _mcp_mesh/shared/fastapi_middleware_manager.py +149 -91
- _mcp_mesh/shared/host_resolver.py +8 -46
- _mcp_mesh/shared/server_discovery.py +115 -86
- _mcp_mesh/shared/simple_shutdown.py +44 -86
- _mcp_mesh/tracing/execution_tracer.py +2 -6
- _mcp_mesh/tracing/redis_metadata_publisher.py +24 -79
- _mcp_mesh/tracing/trace_context_helper.py +3 -13
- _mcp_mesh/tracing/utils.py +29 -15
- _mcp_mesh/utils/fastmcp_schema_extractor.py +2 -1
- {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/METADATA +2 -1
- mcp_mesh-0.8.0b1.dist-info/RECORD +85 -0
- mesh/__init__.py +2 -1
- mesh/decorators.py +89 -5
- _mcp_mesh/generated/.openapi-generator/FILES +0 -50
- _mcp_mesh/generated/.openapi-generator/VERSION +0 -1
- _mcp_mesh/generated/.openapi-generator-ignore +0 -15
- _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -90
- _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -6
- _mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -1088
- _mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -764
- _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -303
- _mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -798
- _mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -21
- _mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -577
- _mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -217
- _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -55
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -158
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -126
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -139
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -92
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -136
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -100
- _mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -107
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -112
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -105
- _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -91
- _mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -111
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -117
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider_resolution_info.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +0 -109
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +0 -139
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +0 -91
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_resolution_info.py +0 -120
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -112
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -129
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -153
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -107
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -117
- _mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -119
- _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +0 -110
- _mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -92
- _mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -259
- _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -418
- _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -117
- _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -140
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -243
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -311
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -386
- _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -104
- _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -396
- _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -116
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -311
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -282
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -98
- _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +0 -84
- _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +0 -264
- _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -79
- _mcp_mesh/pipeline/shared/registry_connection.py +0 -80
- _mcp_mesh/shared/registry_client_wrapper.py +0 -515
- mcp_mesh-0.7.21.dist-info/RECORD +0 -152
- {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,12 +5,15 @@ Optimized for OpenAI models (GPT-4, GPT-4 Turbo, GPT-3.5-turbo)
|
|
|
5
5
|
using OpenAI's native structured output capabilities.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import json
|
|
9
8
|
from typing import Any, Optional
|
|
10
9
|
|
|
11
10
|
from pydantic import BaseModel
|
|
12
11
|
|
|
13
|
-
from .base_provider_handler import
|
|
12
|
+
from .base_provider_handler import (
|
|
13
|
+
BASE_TOOL_INSTRUCTIONS,
|
|
14
|
+
BaseProviderHandler,
|
|
15
|
+
make_schema_strict,
|
|
16
|
+
)
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class OpenAIHandler(BaseProviderHandler):
|
|
@@ -94,8 +97,8 @@ class OpenAIHandler(BaseProviderHandler):
|
|
|
94
97
|
schema = output_type.model_json_schema()
|
|
95
98
|
|
|
96
99
|
# Transform schema for OpenAI strict mode
|
|
97
|
-
# OpenAI requires additionalProperties: false
|
|
98
|
-
schema =
|
|
100
|
+
# OpenAI requires additionalProperties: false and all properties in required
|
|
101
|
+
schema = make_schema_strict(schema, add_all_required=True)
|
|
99
102
|
|
|
100
103
|
# OpenAI structured output format
|
|
101
104
|
# See: https://platform.openai.com/docs/guides/structured-outputs
|
|
@@ -143,14 +146,7 @@ class OpenAIHandler(BaseProviderHandler):
|
|
|
143
146
|
|
|
144
147
|
# Add tool calling instructions if tools available
|
|
145
148
|
if tool_schemas:
|
|
146
|
-
system_content +=
|
|
147
|
-
|
|
148
|
-
IMPORTANT TOOL CALLING RULES:
|
|
149
|
-
- You have access to tools that you can call to gather information
|
|
150
|
-
- Make ONE tool call at a time
|
|
151
|
-
- After receiving tool results, you can make additional calls if needed
|
|
152
|
-
- Once you have all needed information, provide your final response
|
|
153
|
-
"""
|
|
149
|
+
system_content += BASE_TOOL_INSTRUCTIONS
|
|
154
150
|
|
|
155
151
|
# Skip JSON note for str return type (text mode)
|
|
156
152
|
if output_type is str:
|
|
@@ -181,54 +177,3 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
181
177
|
"json_mode": True, # Has dedicated JSON mode via response_format
|
|
182
178
|
}
|
|
183
179
|
|
|
184
|
-
def _add_additional_properties_false(
|
|
185
|
-
self, schema: dict[str, Any]
|
|
186
|
-
) -> dict[str, Any]:
|
|
187
|
-
"""
|
|
188
|
-
Recursively add additionalProperties: false to all object schemas.
|
|
189
|
-
|
|
190
|
-
OpenAI strict mode requires this for all object schemas.
|
|
191
|
-
See: https://platform.openai.com/docs/guides/structured-outputs
|
|
192
|
-
|
|
193
|
-
Args:
|
|
194
|
-
schema: JSON schema from Pydantic model
|
|
195
|
-
|
|
196
|
-
Returns:
|
|
197
|
-
Modified schema with additionalProperties: false on all objects
|
|
198
|
-
"""
|
|
199
|
-
import copy
|
|
200
|
-
|
|
201
|
-
schema = copy.deepcopy(schema)
|
|
202
|
-
self._add_additional_properties_recursive(schema)
|
|
203
|
-
return schema
|
|
204
|
-
|
|
205
|
-
def _add_additional_properties_recursive(self, obj: Any) -> None:
|
|
206
|
-
"""Recursively process schema for OpenAI strict mode compliance."""
|
|
207
|
-
if isinstance(obj, dict):
|
|
208
|
-
# If this is an object type, add additionalProperties: false
|
|
209
|
-
# and ensure required includes all properties
|
|
210
|
-
if obj.get("type") == "object":
|
|
211
|
-
obj["additionalProperties"] = False
|
|
212
|
-
# OpenAI strict mode: required must include ALL property keys
|
|
213
|
-
if "properties" in obj:
|
|
214
|
-
obj["required"] = list(obj["properties"].keys())
|
|
215
|
-
|
|
216
|
-
# Process $defs (Pydantic uses this for nested models)
|
|
217
|
-
if "$defs" in obj:
|
|
218
|
-
for def_schema in obj["$defs"].values():
|
|
219
|
-
self._add_additional_properties_recursive(def_schema)
|
|
220
|
-
|
|
221
|
-
# Process properties
|
|
222
|
-
if "properties" in obj:
|
|
223
|
-
for prop_schema in obj["properties"].values():
|
|
224
|
-
self._add_additional_properties_recursive(prop_schema)
|
|
225
|
-
|
|
226
|
-
# Process items (for arrays)
|
|
227
|
-
if "items" in obj:
|
|
228
|
-
self._add_additional_properties_recursive(obj["items"])
|
|
229
|
-
|
|
230
|
-
# Process anyOf, oneOf, allOf
|
|
231
|
-
for key in ("anyOf", "oneOf", "allOf"):
|
|
232
|
-
if key in obj:
|
|
233
|
-
for item in obj[key]:
|
|
234
|
-
self._add_additional_properties_recursive(item)
|
|
@@ -5,10 +5,11 @@ Manages selection and instantiation of provider handlers based on vendor name.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Optional
|
|
9
9
|
|
|
10
10
|
from .base_provider_handler import BaseProviderHandler
|
|
11
11
|
from .claude_handler import ClaudeHandler
|
|
12
|
+
from .gemini_handler import GeminiHandler
|
|
12
13
|
from .generic_handler import GenericHandler
|
|
13
14
|
from .openai_handler import OpenAIHandler
|
|
14
15
|
|
|
@@ -39,16 +40,17 @@ class ProviderHandlerRegistry:
|
|
|
39
40
|
"""
|
|
40
41
|
|
|
41
42
|
# Built-in vendor mappings
|
|
42
|
-
_handlers:
|
|
43
|
+
_handlers: dict[str, type[BaseProviderHandler]] = {
|
|
43
44
|
"anthropic": ClaudeHandler,
|
|
44
45
|
"openai": OpenAIHandler,
|
|
46
|
+
"gemini": GeminiHandler,
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
# Cache of instantiated handlers (singleton per vendor)
|
|
48
|
-
_instances:
|
|
50
|
+
_instances: dict[str, BaseProviderHandler] = {}
|
|
49
51
|
|
|
50
52
|
@classmethod
|
|
51
|
-
def register(cls, vendor: str, handler_class:
|
|
53
|
+
def register(cls, vendor: str, handler_class: type[BaseProviderHandler]) -> None:
|
|
52
54
|
"""
|
|
53
55
|
Register a custom provider handler.
|
|
54
56
|
|
|
@@ -73,7 +75,9 @@ class ProviderHandlerRegistry:
|
|
|
73
75
|
)
|
|
74
76
|
|
|
75
77
|
cls._handlers[vendor] = handler_class
|
|
76
|
-
logger.info(
|
|
78
|
+
logger.info(
|
|
79
|
+
f"📝 Registered provider handler: {vendor} -> {handler_class.__name__}"
|
|
80
|
+
)
|
|
77
81
|
|
|
78
82
|
# Clear cached instance if it exists (force re-instantiation)
|
|
79
83
|
if vendor in cls._instances:
|
|
@@ -119,9 +123,7 @@ class ProviderHandlerRegistry:
|
|
|
119
123
|
# Get handler class (or fallback to Generic)
|
|
120
124
|
if vendor in cls._handlers:
|
|
121
125
|
handler_class = cls._handlers[vendor]
|
|
122
|
-
logger.info(
|
|
123
|
-
f"✅ Selected {handler_class.__name__} for vendor: {vendor}"
|
|
124
|
-
)
|
|
126
|
+
logger.info(f"✅ Selected {handler_class.__name__} for vendor: {vendor}")
|
|
125
127
|
else:
|
|
126
128
|
handler_class = GenericHandler
|
|
127
129
|
if vendor != "unknown":
|
|
@@ -132,14 +134,18 @@ class ProviderHandlerRegistry:
|
|
|
132
134
|
logger.debug("Using GenericHandler for unknown vendor")
|
|
133
135
|
|
|
134
136
|
# Instantiate and cache
|
|
135
|
-
handler =
|
|
137
|
+
handler = (
|
|
138
|
+
handler_class()
|
|
139
|
+
if handler_class != GenericHandler
|
|
140
|
+
else GenericHandler(vendor)
|
|
141
|
+
)
|
|
136
142
|
cls._instances[vendor] = handler
|
|
137
143
|
|
|
138
144
|
logger.debug(f"🆕 Instantiated handler: {handler}")
|
|
139
145
|
return handler
|
|
140
146
|
|
|
141
147
|
@classmethod
|
|
142
|
-
def list_vendors(cls) ->
|
|
148
|
+
def list_vendors(cls) -> dict[str, str]:
|
|
143
149
|
"""
|
|
144
150
|
List all registered vendors and their handlers.
|
|
145
151
|
|
|
@@ -94,8 +94,9 @@ class ResponseParser:
|
|
|
94
94
|
|
|
95
95
|
Tries multiple strategies to find JSON in mixed responses:
|
|
96
96
|
1. Find ```json ... ``` code fence blocks
|
|
97
|
-
2. Find any JSON object {...}
|
|
98
|
-
3.
|
|
97
|
+
2. Find any JSON object {...} using progressive json.loads
|
|
98
|
+
3. Find any JSON array [...] using progressive json.loads
|
|
99
|
+
4. Return original content if no extraction needed
|
|
99
100
|
|
|
100
101
|
Args:
|
|
101
102
|
content: Raw content that may contain narrative, XML, and JSON
|
|
@@ -109,25 +110,70 @@ class ResponseParser:
|
|
|
109
110
|
extracted = json_match.group(1).strip()
|
|
110
111
|
return extracted
|
|
111
112
|
|
|
112
|
-
# Strategy 2: Try to find
|
|
113
|
-
#
|
|
113
|
+
# Strategy 2: Try to find JSON object using progressive json.loads
|
|
114
|
+
# This correctly handles braces inside string values
|
|
114
115
|
brace_start = content.find("{")
|
|
115
116
|
if brace_start != -1:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
117
|
+
result = ResponseParser._try_progressive_parse(
|
|
118
|
+
content, brace_start, "{", "}"
|
|
119
|
+
)
|
|
120
|
+
if result:
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
# Strategy 3: Try to find JSON array using progressive json.loads
|
|
124
|
+
bracket_start = content.find("[")
|
|
125
|
+
if bracket_start != -1:
|
|
126
|
+
result = ResponseParser._try_progressive_parse(
|
|
127
|
+
content, bracket_start, "[", "]"
|
|
128
|
+
)
|
|
129
|
+
if result:
|
|
130
|
+
return result
|
|
127
131
|
|
|
128
132
|
# No JSON found, return original
|
|
129
133
|
return content
|
|
130
134
|
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _try_progressive_parse(
|
|
137
|
+
content: str, start: int, open_char: str, close_char: str
|
|
138
|
+
) -> str | None:
|
|
139
|
+
"""
|
|
140
|
+
Try to extract valid JSON by progressively extending the end position.
|
|
141
|
+
This correctly handles braces/brackets inside string values.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
content: The full content string
|
|
145
|
+
start: Starting index of the JSON
|
|
146
|
+
open_char: Opening character ('{' or '[')
|
|
147
|
+
close_char: Closing character ('}' or ']')
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Extracted JSON string or None if not found
|
|
151
|
+
"""
|
|
152
|
+
# Find potential end positions based on depth counting
|
|
153
|
+
depth = 0
|
|
154
|
+
potential_ends: list[int] = []
|
|
155
|
+
|
|
156
|
+
for i in range(start, len(content)):
|
|
157
|
+
char = content[i]
|
|
158
|
+
if char == open_char:
|
|
159
|
+
depth += 1
|
|
160
|
+
elif char == close_char:
|
|
161
|
+
depth -= 1
|
|
162
|
+
if depth == 0:
|
|
163
|
+
potential_ends.append(i)
|
|
164
|
+
|
|
165
|
+
# Try each potential end position (shortest first for efficiency)
|
|
166
|
+
for end in potential_ends:
|
|
167
|
+
candidate = content[start : end + 1]
|
|
168
|
+
try:
|
|
169
|
+
json.loads(candidate)
|
|
170
|
+
return candidate
|
|
171
|
+
except json.JSONDecodeError:
|
|
172
|
+
# Not valid JSON, try next potential end
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
return None
|
|
176
|
+
|
|
131
177
|
@staticmethod
|
|
132
178
|
def _strip_markdown_fences(content: str) -> str:
|
|
133
179
|
"""
|
|
@@ -16,6 +16,8 @@ from ..shared.logging_config import (
|
|
|
16
16
|
get_trace_prefix,
|
|
17
17
|
)
|
|
18
18
|
from ..shared.sse_parser import SSEParser
|
|
19
|
+
from ..tracing.context import TraceContext
|
|
20
|
+
from ..tracing.utils import generate_span_id
|
|
19
21
|
|
|
20
22
|
logger = logging.getLogger(__name__)
|
|
21
23
|
|
|
@@ -178,37 +180,6 @@ class UnifiedMCPProxy:
|
|
|
178
180
|
f"FastMCP client failed: {e}"
|
|
179
181
|
) # Convert to ImportError to trigger fallback
|
|
180
182
|
|
|
181
|
-
def _get_trace_headers(self) -> dict[str, str]:
|
|
182
|
-
"""Extract trace headers from current context for distributed tracing.
|
|
183
|
-
|
|
184
|
-
Returns:
|
|
185
|
-
Dict of trace headers or empty dict if no trace context available
|
|
186
|
-
"""
|
|
187
|
-
try:
|
|
188
|
-
from ..tracing.context import TraceContext
|
|
189
|
-
|
|
190
|
-
current_trace = TraceContext.get_current()
|
|
191
|
-
if current_trace:
|
|
192
|
-
headers = {
|
|
193
|
-
"X-Trace-ID": current_trace.trace_id,
|
|
194
|
-
"X-Parent-Span": current_trace.span_id, # Current span becomes parent for downstream
|
|
195
|
-
}
|
|
196
|
-
self.logger.info(
|
|
197
|
-
f"🔗 TRACE_PROPAGATION: Injecting headers trace_id={current_trace.trace_id[:8]}... "
|
|
198
|
-
f"parent_span={current_trace.span_id[:8]}..."
|
|
199
|
-
)
|
|
200
|
-
return headers
|
|
201
|
-
else:
|
|
202
|
-
self.logger.warning("🔗 TRACE_PROPAGATION: No trace context available")
|
|
203
|
-
return {}
|
|
204
|
-
|
|
205
|
-
except Exception as e:
|
|
206
|
-
# Never fail MCP calls due to tracing issues
|
|
207
|
-
self.logger.warning(
|
|
208
|
-
f"🔗 TRACE_PROPAGATION: Exception getting trace context: {e}"
|
|
209
|
-
)
|
|
210
|
-
return {}
|
|
211
|
-
|
|
212
183
|
def _configure_from_kwargs(self):
|
|
213
184
|
"""Auto-configure proxy settings from kwargs."""
|
|
214
185
|
# Basic configuration
|
|
@@ -433,6 +404,19 @@ class UnifiedMCPProxy:
|
|
|
433
404
|
# Get trace prefix if available
|
|
434
405
|
tp = get_trace_prefix()
|
|
435
406
|
|
|
407
|
+
# Inject trace context into arguments for downstream agents
|
|
408
|
+
# This is the fallback mechanism for agents that can't access HTTP headers (e.g., TypeScript)
|
|
409
|
+
args_with_trace = dict(arguments) if arguments else {}
|
|
410
|
+
current_trace = TraceContext.get_current()
|
|
411
|
+
if current_trace:
|
|
412
|
+
# Use current function's span_id as parent for downstream call
|
|
413
|
+
# Don't generate a new span - that creates unpublished "ghost" spans that break the tree
|
|
414
|
+
args_with_trace["_trace_id"] = current_trace.trace_id
|
|
415
|
+
args_with_trace["_parent_span"] = current_trace.span_id
|
|
416
|
+
self.logger.debug(
|
|
417
|
+
f"{tp}🔗 Injecting trace context: trace_id={current_trace.trace_id[:8]}..., parent_span={current_trace.span_id[:8]}..."
|
|
418
|
+
)
|
|
419
|
+
|
|
436
420
|
# Log cross-agent call - summary line
|
|
437
421
|
arg_keys = list(arguments.keys()) if arguments else []
|
|
438
422
|
self.logger.debug(
|
|
@@ -451,7 +435,7 @@ class UnifiedMCPProxy:
|
|
|
451
435
|
async with client_instance as client:
|
|
452
436
|
|
|
453
437
|
# Use FastMCP's call_tool which returns CallToolResult object
|
|
454
|
-
result = await client.call_tool(name,
|
|
438
|
+
result = await client.call_tool(name, args_with_trace)
|
|
455
439
|
|
|
456
440
|
# Calculate performance metrics
|
|
457
441
|
end_time = time.time()
|
|
@@ -479,12 +463,12 @@ class UnifiedMCPProxy:
|
|
|
479
463
|
self.logger.warning(
|
|
480
464
|
f"FastMCP Client not available: {e}, falling back to HTTP"
|
|
481
465
|
)
|
|
482
|
-
return await self._fallback_http_call(name,
|
|
466
|
+
return await self._fallback_http_call(name, args_with_trace)
|
|
483
467
|
except Exception as e:
|
|
484
468
|
self.logger.warning(f"FastMCP Client failed: {e}, falling back to HTTP")
|
|
485
469
|
# Try HTTP fallback
|
|
486
470
|
try:
|
|
487
|
-
result = await self._fallback_http_call(name,
|
|
471
|
+
result = await self._fallback_http_call(name, args_with_trace)
|
|
488
472
|
return result
|
|
489
473
|
except Exception as fallback_error:
|
|
490
474
|
raise RuntimeError(
|
_mcp_mesh/pipeline/__init__.py
CHANGED
|
@@ -2,31 +2,22 @@
|
|
|
2
2
|
MCP Mesh Pipeline Architecture
|
|
3
3
|
|
|
4
4
|
This module provides a clean, explicit pipeline-based architecture for processing
|
|
5
|
-
decorators and managing the mesh agent lifecycle.
|
|
6
|
-
|
|
5
|
+
decorators and managing the mesh agent lifecycle. The Rust core handles registry
|
|
6
|
+
communication including heartbeats, dependency resolution, and deregistration.
|
|
7
7
|
|
|
8
8
|
Key Components:
|
|
9
9
|
- MeshPipeline: Main orchestrator that executes steps in sequence
|
|
10
10
|
- PipelineStep: Interface for individual processing steps
|
|
11
11
|
- PipelineResult: Result container with status and context
|
|
12
|
-
- Built-in steps for common operations (collection, config,
|
|
12
|
+
- Built-in steps for common operations (collection, config, etc.)
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
from .mcp_heartbeat import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
)
|
|
15
|
+
from .mcp_heartbeat import rust_heartbeat_task
|
|
16
|
+
from .mcp_startup import (ConfigurationStep, DecoratorCollectionStep,
|
|
17
|
+
FastAPIServerSetupStep, FastMCPServerDiscoveryStep,
|
|
18
|
+
HeartbeatLoopStep, HeartbeatPreparationStep,
|
|
19
|
+
StartupPipeline)
|
|
20
20
|
from .shared import MeshPipeline, PipelineResult, PipelineStatus, PipelineStep
|
|
21
|
-
from .mcp_startup import (
|
|
22
|
-
ConfigurationStep,
|
|
23
|
-
DecoratorCollectionStep,
|
|
24
|
-
FastAPIServerSetupStep,
|
|
25
|
-
FastMCPServerDiscoveryStep,
|
|
26
|
-
HeartbeatLoopStep,
|
|
27
|
-
HeartbeatPreparationStep,
|
|
28
|
-
StartupPipeline,
|
|
29
|
-
)
|
|
30
21
|
|
|
31
22
|
__all__ = [
|
|
32
23
|
"MeshPipeline",
|
|
@@ -39,8 +30,6 @@ __all__ = [
|
|
|
39
30
|
"FastMCPServerDiscoveryStep",
|
|
40
31
|
"HeartbeatLoopStep",
|
|
41
32
|
"HeartbeatPreparationStep",
|
|
42
|
-
"RegistryConnectionStep",
|
|
43
|
-
"HeartbeatSendStep",
|
|
44
|
-
"DependencyResolutionStep",
|
|
45
33
|
"StartupPipeline",
|
|
34
|
+
"rust_heartbeat_task",
|
|
46
35
|
]
|
|
@@ -3,14 +3,19 @@ API heartbeat pipeline for FastAPI integration.
|
|
|
3
3
|
|
|
4
4
|
Provides periodic service registration and health monitoring
|
|
5
5
|
for FastAPI applications using @mesh.route decorators.
|
|
6
|
+
|
|
7
|
+
Uses Rust core for registry communication, dependency resolution,
|
|
8
|
+
and deregistration.
|
|
6
9
|
"""
|
|
7
10
|
|
|
8
|
-
from .
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
from .api_lifespan_integration import (api_heartbeat_lifespan_task,
|
|
12
|
+
create_api_lifespan_handler,
|
|
13
|
+
integrate_api_heartbeat_with_fastapi)
|
|
14
|
+
from .rust_api_heartbeat import rust_api_heartbeat_task
|
|
11
15
|
|
|
12
16
|
__all__ = [
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
+
"api_heartbeat_lifespan_task",
|
|
18
|
+
"create_api_lifespan_handler",
|
|
19
|
+
"integrate_api_heartbeat_with_fastapi",
|
|
20
|
+
"rust_api_heartbeat_task",
|
|
21
|
+
]
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
2
|
FastAPI lifespan integration for API heartbeat pipeline.
|
|
3
3
|
|
|
4
|
-
Handles the execution of API heartbeat
|
|
4
|
+
Handles the execution of API heartbeat as a background task
|
|
5
5
|
during FastAPI application lifespan for @mesh.route decorator services.
|
|
6
|
+
|
|
7
|
+
Uses the Rust core for registry communication.
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
import asyncio
|
|
@@ -14,65 +16,35 @@ logger = logging.getLogger(__name__)
|
|
|
14
16
|
|
|
15
17
|
async def api_heartbeat_lifespan_task(heartbeat_config: dict[str, Any]) -> None:
|
|
16
18
|
"""
|
|
17
|
-
API heartbeat task that runs in FastAPI lifespan
|
|
19
|
+
API heartbeat task that runs in FastAPI lifespan.
|
|
20
|
+
|
|
21
|
+
Uses Rust-backed heartbeat for registry communication.
|
|
18
22
|
|
|
19
23
|
Args:
|
|
20
|
-
heartbeat_config: Configuration containing service_id, interval,
|
|
24
|
+
heartbeat_config: Configuration containing service_id, interval,
|
|
21
25
|
and context for API heartbeat execution
|
|
22
26
|
"""
|
|
23
|
-
service_id = heartbeat_config
|
|
24
|
-
interval = heartbeat_config["interval"] # Already validated by get_config_value in setup
|
|
25
|
-
context = heartbeat_config["context"]
|
|
27
|
+
service_id = heartbeat_config.get("service_id", "unknown-api-service")
|
|
26
28
|
standalone_mode = heartbeat_config.get("standalone_mode", False)
|
|
27
29
|
|
|
28
30
|
# Check if running in standalone mode
|
|
29
31
|
if standalone_mode:
|
|
30
32
|
logger.info(
|
|
31
|
-
f"💓
|
|
33
|
+
f"💓 API heartbeat in standalone mode for service '{service_id}' "
|
|
32
34
|
f"(no registry communication)"
|
|
33
35
|
)
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
# Create API heartbeat orchestrator for pipeline execution
|
|
37
|
-
from .api_heartbeat_orchestrator import APIHeartbeatOrchestrator
|
|
36
|
+
return
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
# Use Rust-backed heartbeat
|
|
39
|
+
from .rust_api_heartbeat import rust_api_heartbeat_task
|
|
40
40
|
|
|
41
|
-
logger.info(f"💓
|
|
42
|
-
|
|
43
|
-
try:
|
|
44
|
-
while True:
|
|
45
|
-
try:
|
|
46
|
-
# Execute API heartbeat pipeline
|
|
47
|
-
success = await api_heartbeat_orchestrator.execute_api_heartbeat(
|
|
48
|
-
service_id, context
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
if not success:
|
|
52
|
-
# Log failure but continue to next cycle (pipeline handles detailed logging)
|
|
53
|
-
logger.debug(
|
|
54
|
-
f"💔 API heartbeat pipeline failed for service '{service_id}' - "
|
|
55
|
-
f"continuing to next cycle"
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
except Exception as e:
|
|
59
|
-
# Log pipeline execution error but continue to next cycle for resilience
|
|
60
|
-
logger.error(
|
|
61
|
-
f"❌ API heartbeat pipeline execution error for service '{service_id}': {e}"
|
|
62
|
-
)
|
|
63
|
-
# Continue to next cycle - heartbeat should be resilient
|
|
64
|
-
|
|
65
|
-
# Wait for next heartbeat interval
|
|
66
|
-
await asyncio.sleep(interval)
|
|
67
|
-
|
|
68
|
-
except asyncio.CancelledError:
|
|
69
|
-
logger.info(f"🛑 API heartbeat pipeline task cancelled for service '{service_id}'")
|
|
70
|
-
raise
|
|
41
|
+
logger.info(f"💓 Using Rust-backed heartbeat for API service '{service_id}'")
|
|
42
|
+
await rust_api_heartbeat_task(heartbeat_config)
|
|
71
43
|
|
|
72
44
|
|
|
73
45
|
def create_api_lifespan_handler(heartbeat_config: dict[str, Any]) -> Any:
|
|
74
46
|
"""
|
|
75
|
-
Create a FastAPI lifespan context manager that runs API heartbeat
|
|
47
|
+
Create a FastAPI lifespan context manager that runs API heartbeat.
|
|
76
48
|
|
|
77
49
|
Args:
|
|
78
50
|
heartbeat_config: Configuration for API heartbeat execution
|
|
@@ -100,11 +72,13 @@ def create_api_lifespan_handler(heartbeat_config: dict[str, Any]) -> Any:
|
|
|
100
72
|
# Cleanup: cancel heartbeat task
|
|
101
73
|
logger.info(f"🛑 Shutting down FastAPI lifespan for service '{service_id}'")
|
|
102
74
|
heartbeat_task.cancel()
|
|
103
|
-
|
|
75
|
+
|
|
104
76
|
try:
|
|
105
77
|
await heartbeat_task
|
|
106
78
|
except asyncio.CancelledError:
|
|
107
|
-
logger.info(
|
|
79
|
+
logger.info(
|
|
80
|
+
f"✅ API heartbeat task cancelled for service '{service_id}'"
|
|
81
|
+
)
|
|
108
82
|
|
|
109
83
|
return api_lifespan
|
|
110
84
|
|
|
@@ -113,18 +87,18 @@ def integrate_api_heartbeat_with_fastapi(
|
|
|
113
87
|
fastapi_app: Any, heartbeat_config: dict[str, Any]
|
|
114
88
|
) -> None:
|
|
115
89
|
"""
|
|
116
|
-
Integrate API heartbeat
|
|
90
|
+
Integrate API heartbeat with FastAPI lifespan events.
|
|
117
91
|
|
|
118
92
|
Args:
|
|
119
93
|
fastapi_app: FastAPI application instance
|
|
120
94
|
heartbeat_config: Configuration for heartbeat execution
|
|
121
95
|
"""
|
|
122
96
|
service_id = heartbeat_config.get("service_id", "unknown")
|
|
123
|
-
|
|
97
|
+
|
|
124
98
|
try:
|
|
125
99
|
# Check if FastAPI app already has a lifespan handler
|
|
126
100
|
existing_lifespan = getattr(fastapi_app, "router.lifespan_context", None)
|
|
127
|
-
|
|
101
|
+
|
|
128
102
|
if existing_lifespan is not None:
|
|
129
103
|
logger.warning(
|
|
130
104
|
f"⚠️ FastAPI app already has lifespan handler - "
|
|
@@ -144,4 +118,4 @@ def integrate_api_heartbeat_with_fastapi(
|
|
|
144
118
|
f"❌ Failed to integrate API heartbeat with FastAPI lifespan "
|
|
145
119
|
f"for service '{service_id}': {e}"
|
|
146
120
|
)
|
|
147
|
-
raise
|
|
121
|
+
raise
|