mcp-mesh 0.4.1__tar.gz → 0.4.2__tar.gz
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-0.4.1 → mcp_mesh-0.4.2}/PKG-INFO +1 -1
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/__init__.py +1 -1
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/engine/async_mcp_client.py +6 -19
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/engine/full_mcp_proxy.py +25 -20
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/engine/mcp_client_proxy.py +5 -19
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/startup/fastmcpserver_discovery.py +4 -4
- mcp_mesh-0.4.2/_mcp_mesh/shared/sse_parser.py +217 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/mesh/__init__.py +3 -1
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/pyproject.toml +4 -4
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/.gitignore +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/LICENSE +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/README.md +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/engine/__init__.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/engine/decorator_registry.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/engine/dependency_injector.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/engine/http_wrapper.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/engine/self_dependency_proxy.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/engine/session_aware_client.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/engine/session_manager.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/engine/signature_analyzer.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/.openapi-generator/FILES +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/.openapi-generator/VERSION +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/.openapi-generator-ignore +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/__init__.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/heartbeat/__init__.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/heartbeat/dependency_resolution.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/heartbeat/fast_heartbeat_check.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/heartbeat/heartbeat_orchestrator.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/heartbeat/heartbeat_pipeline.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/heartbeat/heartbeat_send.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/heartbeat/lifespan_integration.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/heartbeat/registry_connection.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/shared/__init__.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/shared/base_step.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/shared/pipeline_types.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/shared/registry_connection.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/startup/__init__.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/startup/configuration.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/startup/decorator_collection.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/startup/fastapiserver_setup.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/startup/heartbeat_loop.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/startup/heartbeat_preparation.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/startup/startup_orchestrator.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/pipeline/startup/startup_pipeline.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/shared/__init__.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/shared/config_resolver.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/shared/content_extractor.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/shared/defaults.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/shared/fast_heartbeat_status.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/shared/host_resolver.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/shared/logging_config.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/shared/registry_client_wrapper.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/shared/support_types.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/tracing/agent_context_helper.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/tracing/context.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/tracing/execution_tracer.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/tracing/redis_metadata_publisher.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/tracing/trace_context_helper.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/mesh/decorators.py +0 -0
- {mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/mesh/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-mesh
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -6,6 +6,8 @@ import urllib.error
|
|
|
6
6
|
import urllib.request
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
+
from ..shared.sse_parser import SSEParser
|
|
10
|
+
|
|
9
11
|
logger = logging.getLogger(__name__)
|
|
10
12
|
|
|
11
13
|
|
|
@@ -62,25 +64,10 @@ class AsyncMCPClient:
|
|
|
62
64
|
|
|
63
65
|
response_text = response.text
|
|
64
66
|
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
for line in response_text.split("\n"):
|
|
70
|
-
if line.startswith("data:"):
|
|
71
|
-
json_str = line[5:].strip() # Remove 'data:' prefix
|
|
72
|
-
try:
|
|
73
|
-
json_data = json.loads(json_str)
|
|
74
|
-
break
|
|
75
|
-
except json.JSONDecodeError:
|
|
76
|
-
continue
|
|
77
|
-
|
|
78
|
-
if json_data is None:
|
|
79
|
-
raise RuntimeError("Could not parse SSE response from FastMCP")
|
|
80
|
-
data = json_data
|
|
81
|
-
else:
|
|
82
|
-
# Plain JSON response
|
|
83
|
-
data = response.json()
|
|
67
|
+
# Use shared SSE parser
|
|
68
|
+
data = SSEParser.parse_sse_response(
|
|
69
|
+
response_text, f"AsyncMCPClient.{self.endpoint}"
|
|
70
|
+
)
|
|
84
71
|
|
|
85
72
|
# Check for JSON-RPC error
|
|
86
73
|
if "error" in data:
|
|
@@ -7,6 +7,7 @@ import uuid
|
|
|
7
7
|
from collections.abc import AsyncIterator
|
|
8
8
|
from typing import Any, Optional
|
|
9
9
|
|
|
10
|
+
from ..shared.sse_parser import SSEStreamProcessor
|
|
10
11
|
from .async_mcp_client import AsyncMCPClient
|
|
11
12
|
from .mcp_client_proxy import MCPClientProxy
|
|
12
13
|
|
|
@@ -103,15 +104,18 @@ class FullMCPProxy(MCPClientProxy):
|
|
|
103
104
|
if response.status_code >= 400:
|
|
104
105
|
raise RuntimeError(f"HTTP error {response.status_code}")
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
# Use shared SSE stream processor
|
|
108
|
+
sse_processor = SSEStreamProcessor(f"FullMCPProxy.{name}")
|
|
109
|
+
|
|
110
|
+
async for chunk_bytes in response.aiter_bytes(8192):
|
|
111
|
+
chunks = sse_processor.process_chunk(chunk_bytes)
|
|
112
|
+
for chunk in chunks:
|
|
113
|
+
yield chunk
|
|
114
|
+
|
|
115
|
+
# Process any remaining data
|
|
116
|
+
final_chunks = sse_processor.finalize()
|
|
117
|
+
for chunk in final_chunks:
|
|
118
|
+
yield chunk
|
|
115
119
|
|
|
116
120
|
except ImportError:
|
|
117
121
|
# Fallback: if httpx not available, use sync call
|
|
@@ -588,19 +592,20 @@ class EnhancedFullMCPProxy(FullMCPProxy):
|
|
|
588
592
|
) as response:
|
|
589
593
|
response.raise_for_status()
|
|
590
594
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
buffer += chunk.decode("utf-8")
|
|
595
|
+
# Use shared SSE stream processor
|
|
596
|
+
sse_processor = SSEStreamProcessor(f"EnhancedFullMCPProxy.{name}")
|
|
594
597
|
|
|
595
|
-
|
|
596
|
-
|
|
598
|
+
async for chunk_bytes in response.aiter_bytes(
|
|
599
|
+
max(self.buffer_size, 8192)
|
|
600
|
+
):
|
|
601
|
+
chunks = sse_processor.process_chunk(chunk_bytes)
|
|
602
|
+
for chunk in chunks:
|
|
603
|
+
yield chunk
|
|
597
604
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
except json.JSONDecodeError:
|
|
603
|
-
continue
|
|
605
|
+
# Process any remaining data
|
|
606
|
+
final_chunks = sse_processor.finalize()
|
|
607
|
+
for chunk in final_chunks:
|
|
608
|
+
yield chunk
|
|
604
609
|
|
|
605
610
|
except httpx.TimeoutException:
|
|
606
611
|
raise Exception(f"Streaming timeout after {self.stream_timeout}s")
|
|
@@ -10,6 +10,7 @@ import uuid
|
|
|
10
10
|
from typing import Any, Optional
|
|
11
11
|
|
|
12
12
|
from ..shared.content_extractor import ContentExtractor
|
|
13
|
+
from ..shared.sse_parser import SSEParser
|
|
13
14
|
from .async_mcp_client import AsyncMCPClient
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
@@ -113,25 +114,10 @@ class MCPClientProxy:
|
|
|
113
114
|
with urllib.request.urlopen(req, timeout=30.0) as response:
|
|
114
115
|
response_data = response.read().decode("utf-8")
|
|
115
116
|
|
|
116
|
-
#
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
for line in response_data.split("\n"):
|
|
121
|
-
if line.startswith("data:"):
|
|
122
|
-
json_str = line[5:].strip() # Remove 'data:' prefix
|
|
123
|
-
try:
|
|
124
|
-
json_data = json.loads(json_str)
|
|
125
|
-
break
|
|
126
|
-
except json.JSONDecodeError:
|
|
127
|
-
continue
|
|
128
|
-
|
|
129
|
-
if json_data is None:
|
|
130
|
-
raise RuntimeError("Could not parse SSE response from FastMCP")
|
|
131
|
-
data = json_data
|
|
132
|
-
else:
|
|
133
|
-
# Plain JSON response
|
|
134
|
-
data = json.loads(response_data)
|
|
117
|
+
# Use shared SSE parser
|
|
118
|
+
data = SSEParser.parse_sse_response(
|
|
119
|
+
response_data, f"MCPClientProxy.{self.function_name}"
|
|
120
|
+
)
|
|
135
121
|
|
|
136
122
|
# Check for JSON-RPC error
|
|
137
123
|
if "error" in data:
|
|
@@ -41,7 +41,7 @@ class FastMCPServerDiscoveryStep(PipelineStep):
|
|
|
41
41
|
server_info = []
|
|
42
42
|
total_registered_functions = 0
|
|
43
43
|
|
|
44
|
-
for server_name, server_instance in discovered_servers.items():
|
|
44
|
+
for server_name, server_instance in list(discovered_servers.items()):
|
|
45
45
|
info = self._extract_server_info(server_name, server_instance)
|
|
46
46
|
server_info.append(info)
|
|
47
47
|
total_registered_functions += info.get("function_count", 0)
|
|
@@ -119,7 +119,7 @@ class FastMCPServerDiscoveryStep(PipelineStep):
|
|
|
119
119
|
"pkgutil",
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
for module_name, module in sys.modules.items():
|
|
122
|
+
for module_name, module in list(sys.modules.items()):
|
|
123
123
|
if (
|
|
124
124
|
module
|
|
125
125
|
and not module_name.startswith("_")
|
|
@@ -166,7 +166,7 @@ class FastMCPServerDiscoveryStep(PipelineStep):
|
|
|
166
166
|
module_globals = vars(module)
|
|
167
167
|
# Only log if we find FastMCP instances to reduce noise
|
|
168
168
|
|
|
169
|
-
for var_name, var_value in module_globals.items():
|
|
169
|
+
for var_name, var_value in list(module_globals.items()):
|
|
170
170
|
if self._is_fastmcp_instance(var_value):
|
|
171
171
|
instance_key = f"{module_name}.{var_name}"
|
|
172
172
|
found[instance_key] = var_value
|
|
@@ -230,7 +230,7 @@ class FastMCPServerDiscoveryStep(PipelineStep):
|
|
|
230
230
|
info["function_count"] += len(tools)
|
|
231
231
|
|
|
232
232
|
self.logger.debug(f"Server '{server_name}' has {len(tools)} tools:")
|
|
233
|
-
for tool_name, tool in tools.items():
|
|
233
|
+
for tool_name, tool in list(tools.items()):
|
|
234
234
|
function_ptr = getattr(tool, "fn", None)
|
|
235
235
|
self.logger.debug(f" - {tool_name}: {function_ptr}")
|
|
236
236
|
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Server-Sent Events (SSE) parsing utilities for MCP responses."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SSEParser:
|
|
11
|
+
"""Utility class for parsing Server-Sent Events responses from FastMCP servers.
|
|
12
|
+
|
|
13
|
+
Handles the common issue where large JSON responses get split across multiple
|
|
14
|
+
SSE 'data:' lines, which would cause JSON parsing failures if processed line-by-line.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def parse_sse_response(
|
|
19
|
+
response_text: str, context: str = "unknown"
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
"""
|
|
22
|
+
Parse SSE response text and extract JSON data.
|
|
23
|
+
|
|
24
|
+
Handles multi-line JSON responses by accumulating all 'data:' lines
|
|
25
|
+
before attempting to parse JSON.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
response_text: Raw SSE response text
|
|
29
|
+
context: Context string for error logging
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Parsed JSON data as dictionary
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
RuntimeError: If SSE response cannot be parsed
|
|
36
|
+
"""
|
|
37
|
+
logger.debug(f"🔧 SSEParser.parse_sse_response called from {context}")
|
|
38
|
+
logger.debug(
|
|
39
|
+
f"🔧 Response text length: {len(response_text)}, starts with 'event:': {response_text.startswith('event:')}"
|
|
40
|
+
)
|
|
41
|
+
logger.debug(f"🔧 Response preview: {repr(response_text[:100])}...")
|
|
42
|
+
|
|
43
|
+
# Check if this is SSE format (can be malformed and not start with "event:")
|
|
44
|
+
is_sse_format = (
|
|
45
|
+
response_text.startswith("event:")
|
|
46
|
+
or "event: message" in response_text
|
|
47
|
+
or "data: " in response_text
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if not is_sse_format:
|
|
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(
|
|
54
|
+
f"🔧 {context}: Response preview: {repr(response_text[:200])}..."
|
|
55
|
+
)
|
|
56
|
+
try:
|
|
57
|
+
result = json.loads(response_text)
|
|
58
|
+
logger.debug(f"🔧 {context}: Plain JSON parsed successfully")
|
|
59
|
+
return result
|
|
60
|
+
except json.JSONDecodeError as e:
|
|
61
|
+
logger.error(f"🔧 {context}: Plain JSON parse failed: {e}")
|
|
62
|
+
logger.error(
|
|
63
|
+
f"🔧 {context}: Invalid response content (first 500 chars): {repr(response_text[:500])}"
|
|
64
|
+
)
|
|
65
|
+
raise RuntimeError(f"Invalid JSON response in {context}: {e}")
|
|
66
|
+
|
|
67
|
+
# Parse SSE format: find first valid JSON in data lines
|
|
68
|
+
logger.debug(f"🔧 {context}: Parsing SSE format - looking for first valid JSON")
|
|
69
|
+
data_line_count = 0
|
|
70
|
+
first_valid_json = None
|
|
71
|
+
|
|
72
|
+
for line in response_text.split("\n"):
|
|
73
|
+
if line.startswith("data:"):
|
|
74
|
+
data_content = line[5:].strip() # Remove 'data:' prefix and whitespace
|
|
75
|
+
if data_content:
|
|
76
|
+
data_line_count += 1
|
|
77
|
+
try:
|
|
78
|
+
# Try to parse this line as JSON
|
|
79
|
+
parsed_json = json.loads(data_content)
|
|
80
|
+
if first_valid_json is None:
|
|
81
|
+
first_valid_json = parsed_json
|
|
82
|
+
logger.debug(f"🔧 {context}: Found first valid JSON in data line {data_line_count}")
|
|
83
|
+
except json.JSONDecodeError:
|
|
84
|
+
# 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]}...")
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
logger.debug(
|
|
89
|
+
f"🔧 {context}: Processed {data_line_count} data lines"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Return first valid JSON found
|
|
93
|
+
if first_valid_json is None:
|
|
94
|
+
logger.error(f"🔧 {context}: No valid JSON found in SSE response")
|
|
95
|
+
raise RuntimeError(f"Could not parse SSE response from FastMCP")
|
|
96
|
+
|
|
97
|
+
logger.debug(
|
|
98
|
+
f"🔧 {context}: SSE parsing successful! Result type: {type(first_valid_json)}"
|
|
99
|
+
)
|
|
100
|
+
return first_valid_json
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def parse_streaming_sse_chunk(chunk_data: str) -> Optional[dict[str, Any]]:
|
|
104
|
+
"""
|
|
105
|
+
Parse a single streaming SSE chunk.
|
|
106
|
+
|
|
107
|
+
Used for processing individual chunks in streaming responses.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
chunk_data: Single data line content (without 'data:' prefix)
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Parsed JSON if valid and complete, None if should be skipped
|
|
114
|
+
"""
|
|
115
|
+
if not chunk_data.strip():
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
# Quick validation for complete JSON structures
|
|
119
|
+
chunk_data = chunk_data.strip()
|
|
120
|
+
|
|
121
|
+
# Must be complete JSON structures
|
|
122
|
+
if (
|
|
123
|
+
(chunk_data.startswith("{") and not chunk_data.endswith("}"))
|
|
124
|
+
or (chunk_data.startswith("[") and not chunk_data.endswith("]"))
|
|
125
|
+
or (chunk_data.startswith('"') and not chunk_data.endswith('"'))
|
|
126
|
+
):
|
|
127
|
+
# Incomplete JSON structure - should be accumulated elsewhere
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
return json.loads(chunk_data)
|
|
132
|
+
except json.JSONDecodeError:
|
|
133
|
+
# Invalid JSON - skip this chunk
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class SSEStreamProcessor:
|
|
138
|
+
"""Processor for streaming SSE responses with proper buffering."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, context: str = "streaming"):
|
|
141
|
+
self.context = context
|
|
142
|
+
self.buffer = ""
|
|
143
|
+
self.logger = logger.getChild(f"sse_stream.{context}")
|
|
144
|
+
|
|
145
|
+
def process_chunk(self, chunk_bytes: bytes) -> list[dict[str, Any]]:
|
|
146
|
+
"""
|
|
147
|
+
Process a chunk of bytes and return any complete JSON objects found.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
chunk_bytes: Raw bytes from streaming response
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List of complete JSON objects found in this chunk
|
|
154
|
+
"""
|
|
155
|
+
self.logger.debug(
|
|
156
|
+
f"🌊 SSEStreamProcessor.process_chunk called for {self.context}, chunk size: {len(chunk_bytes)}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
chunk_text = chunk_bytes.decode("utf-8")
|
|
161
|
+
self.buffer += chunk_text
|
|
162
|
+
self.logger.debug(
|
|
163
|
+
f"🌊 {self.context}: Buffer size after chunk: {len(self.buffer)}"
|
|
164
|
+
)
|
|
165
|
+
except UnicodeDecodeError:
|
|
166
|
+
self.logger.warning(
|
|
167
|
+
f"🌊 {self.context}: Skipping chunk with unicode decode error"
|
|
168
|
+
)
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
results = []
|
|
172
|
+
events_processed = 0
|
|
173
|
+
|
|
174
|
+
# Process complete SSE events (end with \n\n)
|
|
175
|
+
while True:
|
|
176
|
+
event_end = self.buffer.find("\n\n")
|
|
177
|
+
if event_end == -1:
|
|
178
|
+
break # No complete event yet
|
|
179
|
+
|
|
180
|
+
event_block = self.buffer[:event_end]
|
|
181
|
+
self.buffer = self.buffer[event_end + 2 :] # Remove processed event
|
|
182
|
+
events_processed += 1
|
|
183
|
+
|
|
184
|
+
# Extract data from SSE event
|
|
185
|
+
for line in event_block.split("\n"):
|
|
186
|
+
if line.startswith("data: "):
|
|
187
|
+
data_str = line[6:].strip() # Remove "data: " prefix
|
|
188
|
+
if data_str:
|
|
189
|
+
parsed = SSEParser.parse_streaming_sse_chunk(data_str)
|
|
190
|
+
if parsed:
|
|
191
|
+
results.append(parsed)
|
|
192
|
+
|
|
193
|
+
self.logger.debug(
|
|
194
|
+
f"🌊 {self.context}: Processed {events_processed} complete SSE events, yielding {len(results)} JSON objects"
|
|
195
|
+
)
|
|
196
|
+
return results
|
|
197
|
+
|
|
198
|
+
def finalize(self) -> list[dict[str, Any]]:
|
|
199
|
+
"""
|
|
200
|
+
Process any remaining data in buffer.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
List of any final JSON objects found
|
|
204
|
+
"""
|
|
205
|
+
results = []
|
|
206
|
+
|
|
207
|
+
if self.buffer.strip():
|
|
208
|
+
for line in self.buffer.split("\n"):
|
|
209
|
+
if line.startswith("data: "):
|
|
210
|
+
data_str = line[6:].strip()
|
|
211
|
+
if data_str:
|
|
212
|
+
parsed = SSEParser.parse_streaming_sse_chunk(data_str)
|
|
213
|
+
if parsed:
|
|
214
|
+
results.append(parsed)
|
|
215
|
+
|
|
216
|
+
self.buffer = "" # Clear buffer
|
|
217
|
+
return results
|
|
@@ -19,7 +19,7 @@ Use 'import mesh' and then '@mesh.tool()' for consistency with MCP patterns.
|
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
21
|
from . import decorators
|
|
22
|
-
from .types import McpMeshAgent
|
|
22
|
+
from .types import McpAgent, McpMeshAgent
|
|
23
23
|
|
|
24
24
|
__version__ = "1.0.0"
|
|
25
25
|
|
|
@@ -95,6 +95,8 @@ def __getattr__(name):
|
|
|
95
95
|
return decorators.agent
|
|
96
96
|
elif name == "McpMeshAgent":
|
|
97
97
|
return McpMeshAgent
|
|
98
|
+
elif name == "McpAgent":
|
|
99
|
+
return McpAgent
|
|
98
100
|
elif name == "create_server":
|
|
99
101
|
return create_server
|
|
100
102
|
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "mcp-mesh"
|
|
9
|
-
version = "0.4.
|
|
9
|
+
version = "0.4.2"
|
|
10
10
|
description = "Kubernetes-native platform for distributed MCP applications"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
license = { text = "MIT" }
|
|
@@ -117,7 +117,7 @@ extend-exclude = '''
|
|
|
117
117
|
'''
|
|
118
118
|
|
|
119
119
|
[tool.ruff]
|
|
120
|
-
target-version = "0.4.
|
|
120
|
+
target-version = "0.4.2"
|
|
121
121
|
line-length = 88
|
|
122
122
|
|
|
123
123
|
[tool.ruff.lint]
|
|
@@ -154,7 +154,7 @@ ignore = [
|
|
|
154
154
|
"tests/**" = ["E712", "F841", "B007", "C401", "F401"] # Relax style requirements for test files
|
|
155
155
|
|
|
156
156
|
[tool.mypy]
|
|
157
|
-
python_version = "0.4.
|
|
157
|
+
python_version = "0.4.2"
|
|
158
158
|
check_untyped_defs = false # Temporarily relaxed
|
|
159
159
|
disallow_any_generics = false # Temporarily relaxed
|
|
160
160
|
disallow_incomplete_defs = false # Temporarily relaxed
|
|
@@ -166,7 +166,7 @@ warn_return_any = false # Temporarily relaxed
|
|
|
166
166
|
exclude = ["tests/", ".*agent_server_generated.*", ".*registry_client_generated.*"] # Skip type checking for test and generated files
|
|
167
167
|
|
|
168
168
|
[tool.pytest.ini_options]
|
|
169
|
-
minversion = "0.4.
|
|
169
|
+
minversion = "0.4.2"
|
|
170
170
|
addopts = "-ra -q --strict-markers --strict-config"
|
|
171
171
|
testpaths = [
|
|
172
172
|
"tests",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py
RENAMED
|
File without changes
|
{mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py
RENAMED
|
File without changes
|
{mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py
RENAMED
|
File without changes
|
{mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/api_client.py
RENAMED
|
File without changes
|
{mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/api_response.py
RENAMED
|
File without changes
|
{mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/configuration.py
RENAMED
|
File without changes
|
{mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py
RENAMED
|
File without changes
|
{mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py
RENAMED
|
File without changes
|
{mcp_mesh-0.4.1 → mcp_mesh-0.4.2}/_mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|