solace-agent-mesh 1.0.6__py3-none-any.whl → 1.0.8__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.
Potentially problematic release.
This version of solace-agent-mesh might be problematic. Click here for more details.
- solace_agent_mesh/agent/adk/artifacts/__init__.py +1 -0
- solace_agent_mesh/agent/adk/{filesystem_artifact_service.py → artifacts/filesystem_artifact_service.py} +14 -15
- solace_agent_mesh/agent/adk/artifacts/s3_artifact_service.py +440 -0
- solace_agent_mesh/agent/adk/callbacks.py +123 -159
- solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +316 -0
- solace_agent_mesh/agent/adk/intelligent_mcp_callbacks.py +414 -0
- solace_agent_mesh/agent/adk/mcp_content_processor.py +665 -0
- solace_agent_mesh/agent/adk/services.py +43 -1
- solace_agent_mesh/agent/adk/setup.py +85 -45
- solace_agent_mesh/agent/adk/tool_wrapper.py +19 -3
- solace_agent_mesh/agent/protocol/event_handlers.py +1 -1
- solace_agent_mesh/agent/sac/app.py +67 -0
- solace_agent_mesh/agent/sac/component.py +14 -86
- solace_agent_mesh/assets/docs/404.html +3 -3
- solace_agent_mesh/assets/docs/assets/js/04989206.b9dfe831.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/0e682baa.b3bbde9a.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/1023fc19.364235d5.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/1523c6b4.1b0ec6f9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/166ab619.e8f3a7c7.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/21ceee5f.3bf39250.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3d406171.7d02a73b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/42b3f8d8.8ccb9901.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/442a8107.b3159bb2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/4c2787c2.fc6804f2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/5b4258a4.0d080cd9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/75384d09.ccd480c4.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/768e31b0.8b51cd70.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/945fb41e.c63791d1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{9eff14a2.036c35ea.js → 9eff14a2.472b0310.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/a3a92b25.4b7fa6a2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/aba87c2f.76376d7c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ae4415af.7a2f0bbf.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/b7006a3a.73a79653.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/beecea0d.ae31f6a7.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/c2c06897.587b4af5.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{cd3d4052.ca6eed8c.js → cd3d4052.b6535013.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/f284c35a.731836ad.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/f897a61a.0aa29dbb.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/main.6dba4a66.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/runtime~main.6415ad00.js +1 -0
- solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +28 -4
- solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +6 -6
- solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +8 -8
- solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +34 -5
- solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +6 -6
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +72 -0
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +7 -7
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +35 -16
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +17 -11
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +6 -6
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +6 -6
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +6 -6
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +8 -8
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +14 -14
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +8 -8
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +6 -6
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +35 -23
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +6 -6
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +4 -4
- solace_agent_mesh/assets/docs/lunr-index-1756153049706.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1756153049706.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -1
- solace_agent_mesh/assets/docs/sitemap.xml +1 -1
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/cli/commands/add_cmd/add_cmd_llm.txt +1 -1
- solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +67 -10
- solace_agent_mesh/cli/commands/add_cmd/gateway_cmd.py +2 -2
- solace_agent_mesh/cli/commands/eval_cmd.py +8 -2
- solace_agent_mesh/cli/commands/init_cmd/__init__.py +20 -2
- solace_agent_mesh/cli/commands/init_cmd/env_step.py +25 -1
- solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +45 -1
- solace_agent_mesh/cli/utils.py +21 -12
- solace_agent_mesh/client/webui/frontend/static/assets/main-BucUdn9m.js +673 -0
- solace_agent_mesh/client/webui/frontend/static/index.html +1 -1
- solace_agent_mesh/common/a2a_protocol.py +1 -1
- solace_agent_mesh/common/utils/mime_helpers.py +60 -1
- solace_agent_mesh/config_portal/backend/server.py +1 -1
- solace_agent_mesh/config_portal/frontend/static/client/assets/{_index-xSu2leR8.js → _index-MqsrTd6g.js} +9 -9
- solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-950eb3be.js → manifest-28271392.js} +1 -1
- solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
- solace_agent_mesh/core_a2a/core_a2a_llm.txt +1 -1
- solace_agent_mesh/core_a2a/service.py +1 -1
- solace_agent_mesh/evaluation/run.py +149 -15
- solace_agent_mesh/evaluation/summary_builder.py +5 -3
- solace_agent_mesh/gateway/http_sse/dependencies.py +1 -1
- solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +1 -1
- solace_agent_mesh/gateway/http_sse/services/task_service.py +1 -1
- solace_agent_mesh/llm_detail.txt +2 -2
- solace_agent_mesh/templates/agent_template.yaml +1 -1
- solace_agent_mesh/templates/plugin_agent_config_template.yaml +3 -3
- solace_agent_mesh/templates/plugin_readme_template.md +1 -1
- solace_agent_mesh/templates/shared_config.yaml +8 -1
- {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.8.dist-info}/METADATA +4 -1
- {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.8.dist-info}/RECORD +113 -108
- solace_agent_mesh/assets/docs/assets/js/04989206.da8246cd.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/0e682baa.79f0ab22.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/1023fc19.8e6d174c.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/1523c6b4.91c7bc01.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/166ab619.7d97ccaf.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/21ceee5f.614fa8dd.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/3d406171.9b081d5f.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/42b3f8d8.36090198.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/442a8107.5ba94b65.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/4c2787c2.66ee00e9.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/5b4258a4.bda20761.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/75384d09.c3991823.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/768e31b0.a12673db.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/945fb41e.74d728aa.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/a3a92b25.26ca071f.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/aba87c2f.a6b84da6.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/ae4415af.96189a93.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/b7006a3a.38c0cf3d.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/bb2ef573.56931473.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/c2c06897.63b76e9e.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/f284c35a.5aff74ab.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/f897a61a.862b0514.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/main.ea9672b6.js +0 -2
- solace_agent_mesh/assets/docs/assets/js/runtime~main.aa687c82.js +0 -1
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +0 -17
- solace_agent_mesh/assets/docs/lunr-index-1755285974624.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1755285974624.json +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-DzKPMTRs.js +0 -673
- /solace_agent_mesh/assets/docs/assets/js/{main.ea9672b6.js.LICENSE.txt → main.6dba4a66.js.LICENSE.txt} +0 -0
- {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.8.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.8.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Content Processor for Intelligent Artifact Saving
|
|
3
|
+
|
|
4
|
+
This module provides intelligent processing of MCP tool responses, converting
|
|
5
|
+
raw MCP content into appropriately typed and formatted artifacts based on
|
|
6
|
+
the MCP specification content types.
|
|
7
|
+
|
|
8
|
+
Supports:
|
|
9
|
+
- Text content with format detection (CSV, JSON, YAML)
|
|
10
|
+
- Image content with base64 decoding
|
|
11
|
+
- Audio content with base64 decoding
|
|
12
|
+
- Resource content with URI-based filename extraction
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import base64
|
|
16
|
+
import csv
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
import uuid
|
|
20
|
+
import yaml
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from io import StringIO
|
|
23
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
24
|
+
from urllib.parse import urlparse
|
|
25
|
+
|
|
26
|
+
from solace_ai_connector.common.log import log
|
|
27
|
+
|
|
28
|
+
from ...common.utils.mime_helpers import (
|
|
29
|
+
is_text_based_mime_type,
|
|
30
|
+
get_extension_for_mime_type,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MCPContentType:
|
|
35
|
+
"""Constants for MCP content types as defined in the MCP specification."""
|
|
36
|
+
|
|
37
|
+
TEXT = "text"
|
|
38
|
+
IMAGE = "image"
|
|
39
|
+
AUDIO = "audio"
|
|
40
|
+
RESOURCE = "resource"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TextFormat:
|
|
44
|
+
"""Constants for detected text formats."""
|
|
45
|
+
|
|
46
|
+
CSV = "csv"
|
|
47
|
+
JSON = "json"
|
|
48
|
+
YAML = "yaml"
|
|
49
|
+
PLAIN = "plain"
|
|
50
|
+
MARKDOWN = "markdown"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MCPContentItem:
|
|
54
|
+
"""Represents a processed MCP content item with metadata."""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
content_type: str,
|
|
59
|
+
content_bytes: bytes,
|
|
60
|
+
mime_type: str,
|
|
61
|
+
filename: str,
|
|
62
|
+
metadata: Dict[str, Any],
|
|
63
|
+
original_content: Dict[str, Any],
|
|
64
|
+
):
|
|
65
|
+
self.content_type = content_type
|
|
66
|
+
self.content_bytes = content_bytes
|
|
67
|
+
self.mime_type = mime_type
|
|
68
|
+
self.filename = filename
|
|
69
|
+
self.metadata = metadata
|
|
70
|
+
self.original_content = original_content
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class MCPContentProcessor:
|
|
74
|
+
"""Main processor for MCP tool response content."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, tool_name: str, tool_args: Dict[str, Any]):
|
|
77
|
+
self.tool_name = tool_name
|
|
78
|
+
self.tool_args = tool_args
|
|
79
|
+
self.log_identifier = f"[MCPContentProcessor:{tool_name}]"
|
|
80
|
+
|
|
81
|
+
def process_mcp_response(
|
|
82
|
+
self, mcp_response_dict: Dict[str, Any]
|
|
83
|
+
) -> List[MCPContentItem]:
|
|
84
|
+
"""
|
|
85
|
+
Process an MCP tool response and extract content items.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
mcp_response_dict: The raw MCP tool response dictionary
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List of processed MCPContentItem objects
|
|
92
|
+
"""
|
|
93
|
+
log.debug(
|
|
94
|
+
"%s Processing MCP response for intelligent artifact saving",
|
|
95
|
+
self.log_identifier,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
content_items = []
|
|
99
|
+
|
|
100
|
+
# Extract content array from MCP response
|
|
101
|
+
content_array = self._extract_content_array(mcp_response_dict)
|
|
102
|
+
|
|
103
|
+
if not content_array:
|
|
104
|
+
log.warning(
|
|
105
|
+
"%s No content array found in MCP response", self.log_identifier
|
|
106
|
+
)
|
|
107
|
+
return content_items
|
|
108
|
+
|
|
109
|
+
# Process each content item
|
|
110
|
+
for idx, content_item in enumerate(content_array):
|
|
111
|
+
try:
|
|
112
|
+
processed_item = self._process_content_item(content_item, idx)
|
|
113
|
+
if processed_item:
|
|
114
|
+
content_items.append(processed_item)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
log.exception(
|
|
117
|
+
"%s Error processing content item %d: %s",
|
|
118
|
+
self.log_identifier,
|
|
119
|
+
idx,
|
|
120
|
+
e,
|
|
121
|
+
)
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
log.info(
|
|
125
|
+
"%s Successfully processed %d content items from MCP response",
|
|
126
|
+
self.log_identifier,
|
|
127
|
+
len(content_items),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return content_items
|
|
131
|
+
|
|
132
|
+
def _extract_content_array(
|
|
133
|
+
self, mcp_response_dict: Dict[str, Any]
|
|
134
|
+
) -> List[Dict[str, Any]]:
|
|
135
|
+
"""Extract the content array from various possible MCP response structures."""
|
|
136
|
+
|
|
137
|
+
# Try common MCP response structures
|
|
138
|
+
if "content" in mcp_response_dict:
|
|
139
|
+
content = mcp_response_dict["content"]
|
|
140
|
+
if isinstance(content, list):
|
|
141
|
+
return content
|
|
142
|
+
elif isinstance(content, dict):
|
|
143
|
+
return [content]
|
|
144
|
+
|
|
145
|
+
# Check for nested structures
|
|
146
|
+
if "result" in mcp_response_dict:
|
|
147
|
+
result = mcp_response_dict["result"]
|
|
148
|
+
if isinstance(result, dict) and "content" in result:
|
|
149
|
+
content = result["content"]
|
|
150
|
+
if isinstance(content, list):
|
|
151
|
+
return content
|
|
152
|
+
elif isinstance(content, dict):
|
|
153
|
+
return [content]
|
|
154
|
+
|
|
155
|
+
# Check for direct content items at root level
|
|
156
|
+
if "type" in mcp_response_dict:
|
|
157
|
+
return [mcp_response_dict]
|
|
158
|
+
|
|
159
|
+
log.debug("%s No recognizable content structure found", self.log_identifier)
|
|
160
|
+
return []
|
|
161
|
+
|
|
162
|
+
def _process_content_item(
|
|
163
|
+
self, content_item: Dict[str, Any], index: int
|
|
164
|
+
) -> Optional[MCPContentItem]:
|
|
165
|
+
"""Process a single content item based on its type."""
|
|
166
|
+
|
|
167
|
+
content_type = content_item.get("type")
|
|
168
|
+
if not content_type:
|
|
169
|
+
log.warning(
|
|
170
|
+
"%s Content item %d missing type field", self.log_identifier, index
|
|
171
|
+
)
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
log.debug(
|
|
175
|
+
"%s Processing content item %d of type: %s",
|
|
176
|
+
self.log_identifier,
|
|
177
|
+
index,
|
|
178
|
+
content_type,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Route to appropriate processor based on content type
|
|
182
|
+
if content_type == MCPContentType.TEXT:
|
|
183
|
+
return self._process_text_content(content_item, index)
|
|
184
|
+
elif content_type == MCPContentType.IMAGE:
|
|
185
|
+
return self._process_image_content(content_item, index)
|
|
186
|
+
elif content_type == MCPContentType.AUDIO:
|
|
187
|
+
return self._process_audio_content(content_item, index)
|
|
188
|
+
elif content_type == MCPContentType.RESOURCE:
|
|
189
|
+
return self._process_resource_content(content_item, index)
|
|
190
|
+
else:
|
|
191
|
+
log.warning(
|
|
192
|
+
"%s Unknown content type: %s", self.log_identifier, content_type
|
|
193
|
+
)
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def _log_empty_content(self, content_type: str, index: int):
|
|
197
|
+
"""Log warning for empty content and return None."""
|
|
198
|
+
log.warning(
|
|
199
|
+
"%s %s content item %d is empty", self.log_identifier, content_type, index
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def _create_content_item(
|
|
203
|
+
self,
|
|
204
|
+
content_type: str,
|
|
205
|
+
content_bytes: bytes,
|
|
206
|
+
mime_type: str,
|
|
207
|
+
filename: str,
|
|
208
|
+
index: int,
|
|
209
|
+
specific_metadata: Dict[str, Any],
|
|
210
|
+
original_content: Dict[str, Any],
|
|
211
|
+
) -> MCPContentItem:
|
|
212
|
+
"""Create an MCPContentItem with common metadata structure."""
|
|
213
|
+
# Create base metadata that's common to all content types
|
|
214
|
+
metadata = {
|
|
215
|
+
"description": f"{content_type.title()} content from MCP tool {self.tool_name}",
|
|
216
|
+
"source_tool_name": self.tool_name,
|
|
217
|
+
"source_tool_args": self.tool_args,
|
|
218
|
+
"content_type": content_type,
|
|
219
|
+
"content_index": index,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# Add MIME type info for binary content
|
|
223
|
+
if content_type in [
|
|
224
|
+
MCPContentType.IMAGE,
|
|
225
|
+
MCPContentType.AUDIO,
|
|
226
|
+
MCPContentType.RESOURCE,
|
|
227
|
+
]:
|
|
228
|
+
metadata["original_mime_type"] = mime_type
|
|
229
|
+
|
|
230
|
+
# Merge in specific metadata
|
|
231
|
+
metadata.update(specific_metadata)
|
|
232
|
+
|
|
233
|
+
return MCPContentItem(
|
|
234
|
+
content_type=content_type,
|
|
235
|
+
content_bytes=content_bytes,
|
|
236
|
+
mime_type=mime_type,
|
|
237
|
+
filename=filename,
|
|
238
|
+
metadata=metadata,
|
|
239
|
+
original_content=original_content,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def _process_binary_content(
|
|
243
|
+
self,
|
|
244
|
+
content_item: Dict[str, Any],
|
|
245
|
+
index: int,
|
|
246
|
+
content_type: str,
|
|
247
|
+
data_key: str,
|
|
248
|
+
mime_type_key: str,
|
|
249
|
+
default_mime_type: str,
|
|
250
|
+
default_extension: str,
|
|
251
|
+
size_metadata_key: str,
|
|
252
|
+
) -> Optional[MCPContentItem]:
|
|
253
|
+
"""Generic processor for binary content (images, audio) with base64 decoding."""
|
|
254
|
+
binary_data = content_item.get(data_key, "")
|
|
255
|
+
mime_type = content_item.get(mime_type_key, default_mime_type)
|
|
256
|
+
|
|
257
|
+
if not binary_data:
|
|
258
|
+
return self._log_empty_content(content_type.title(), index)
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
# Decode base64 data
|
|
262
|
+
content_bytes = base64.b64decode(binary_data)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
log.error(
|
|
265
|
+
"%s Failed to decode base64 %s data for item %d: %s",
|
|
266
|
+
self.log_identifier,
|
|
267
|
+
content_type,
|
|
268
|
+
index,
|
|
269
|
+
e,
|
|
270
|
+
)
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
# Generate filename with appropriate extension
|
|
274
|
+
extension = get_extension_for_mime_type(mime_type, default_extension)
|
|
275
|
+
filename = (
|
|
276
|
+
f"{self.tool_name}_{content_type}_{index}_{uuid.uuid4().hex[:8]}{extension}"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Create specific metadata for binary content
|
|
280
|
+
specific_metadata = {
|
|
281
|
+
size_metadata_key: len(content_bytes),
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
log.debug(
|
|
285
|
+
"%s Processed %s content item %d: mime_type=%s, size=%d bytes",
|
|
286
|
+
self.log_identifier,
|
|
287
|
+
content_type,
|
|
288
|
+
index,
|
|
289
|
+
mime_type,
|
|
290
|
+
len(content_bytes),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return self._create_content_item(
|
|
294
|
+
content_type=content_type,
|
|
295
|
+
content_bytes=content_bytes,
|
|
296
|
+
mime_type=mime_type,
|
|
297
|
+
filename=filename,
|
|
298
|
+
index=index,
|
|
299
|
+
specific_metadata=specific_metadata,
|
|
300
|
+
original_content=content_item,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def _process_text_content(
|
|
304
|
+
self, content_item: Dict[str, Any], index: int
|
|
305
|
+
) -> Optional[MCPContentItem]:
|
|
306
|
+
"""Process text content with format detection and parsing."""
|
|
307
|
+
text_content = content_item.get("text", "")
|
|
308
|
+
if not text_content:
|
|
309
|
+
return self._log_empty_content("Text", index)
|
|
310
|
+
|
|
311
|
+
# Detect text format
|
|
312
|
+
detected_format, parse_success, parsed_data = (
|
|
313
|
+
self._detect_and_parse_text_format(text_content)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Determine MIME type and file extension
|
|
317
|
+
mime_type, extension = self._get_text_mime_type_and_extension(detected_format)
|
|
318
|
+
|
|
319
|
+
# Generate filename
|
|
320
|
+
filename = self._generate_text_filename(detected_format, extension, index)
|
|
321
|
+
|
|
322
|
+
# Create specific metadata for text content
|
|
323
|
+
specific_metadata = {
|
|
324
|
+
"detected_format": detected_format,
|
|
325
|
+
"format_parse_success": parse_success,
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if parse_success and parsed_data is not None:
|
|
329
|
+
specific_metadata["parsed_data_summary"] = self._create_parsed_data_summary(
|
|
330
|
+
parsed_data, detected_format
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# If CSV was detected, use the version with unescaped newlines for saving.
|
|
334
|
+
if detected_format == TextFormat.CSV:
|
|
335
|
+
content_to_save = text_content.replace("\\n", "\n")
|
|
336
|
+
else:
|
|
337
|
+
content_to_save = text_content
|
|
338
|
+
|
|
339
|
+
# Convert to bytes
|
|
340
|
+
content_bytes = content_to_save.encode("utf-8")
|
|
341
|
+
|
|
342
|
+
log.debug(
|
|
343
|
+
"%s Processed text content item %d: format=%s, parse_success=%s, size=%d bytes",
|
|
344
|
+
self.log_identifier,
|
|
345
|
+
index,
|
|
346
|
+
detected_format,
|
|
347
|
+
parse_success,
|
|
348
|
+
len(content_bytes),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return self._create_content_item(
|
|
352
|
+
content_type=MCPContentType.TEXT,
|
|
353
|
+
content_bytes=content_bytes,
|
|
354
|
+
mime_type=mime_type,
|
|
355
|
+
filename=filename,
|
|
356
|
+
index=index,
|
|
357
|
+
specific_metadata=specific_metadata,
|
|
358
|
+
original_content=content_item,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
def _process_image_content(
|
|
362
|
+
self, content_item: Dict[str, Any], index: int
|
|
363
|
+
) -> Optional[MCPContentItem]:
|
|
364
|
+
"""Process image content with base64 decoding."""
|
|
365
|
+
return self._process_binary_content(
|
|
366
|
+
content_item=content_item,
|
|
367
|
+
index=index,
|
|
368
|
+
content_type=MCPContentType.IMAGE,
|
|
369
|
+
data_key="data",
|
|
370
|
+
mime_type_key="mimeType",
|
|
371
|
+
default_mime_type="image/png",
|
|
372
|
+
default_extension=".png",
|
|
373
|
+
size_metadata_key="image_size_bytes",
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def _process_audio_content(
|
|
377
|
+
self, content_item: Dict[str, Any], index: int
|
|
378
|
+
) -> Optional[MCPContentItem]:
|
|
379
|
+
"""Process audio content with base64 decoding."""
|
|
380
|
+
return self._process_binary_content(
|
|
381
|
+
content_item=content_item,
|
|
382
|
+
index=index,
|
|
383
|
+
content_type=MCPContentType.AUDIO,
|
|
384
|
+
data_key="data",
|
|
385
|
+
mime_type_key="mimeType",
|
|
386
|
+
default_mime_type="audio/wav",
|
|
387
|
+
default_extension=".wav",
|
|
388
|
+
size_metadata_key="audio_size_bytes",
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def _process_resource_content(
|
|
392
|
+
self, content_item: Dict[str, Any], index: int
|
|
393
|
+
) -> Optional[MCPContentItem]:
|
|
394
|
+
"""Process resource content with URI-based filename extraction and MIME type detection."""
|
|
395
|
+
resource = content_item.get("resource", {})
|
|
396
|
+
if not resource:
|
|
397
|
+
log.warning(
|
|
398
|
+
"%s Resource content item %d missing resource field",
|
|
399
|
+
self.log_identifier,
|
|
400
|
+
index,
|
|
401
|
+
)
|
|
402
|
+
return None
|
|
403
|
+
|
|
404
|
+
uri = resource.get("uri", "")
|
|
405
|
+
mime_type = resource.get("mimeType", "application/octet-stream")
|
|
406
|
+
text_content = resource.get("text")
|
|
407
|
+
blob_content = resource.get("blob")
|
|
408
|
+
|
|
409
|
+
if uri:
|
|
410
|
+
uri = str(uri)
|
|
411
|
+
|
|
412
|
+
# Extract filename from URI
|
|
413
|
+
filename = self._extract_filename_from_uri(uri, mime_type, index)
|
|
414
|
+
|
|
415
|
+
# Determine if the resource is text-based or binary using MIME type detection
|
|
416
|
+
is_text_based = is_text_based_mime_type(mime_type)
|
|
417
|
+
|
|
418
|
+
if blob_content:
|
|
419
|
+
# Handle binary blob content
|
|
420
|
+
try:
|
|
421
|
+
content_bytes = base64.b64decode(blob_content)
|
|
422
|
+
specific_metadata = {
|
|
423
|
+
"resource_uri": uri,
|
|
424
|
+
"has_text_content": False,
|
|
425
|
+
"has_blob_content": True,
|
|
426
|
+
"is_text_based": False,
|
|
427
|
+
"decoded_from_base64": True,
|
|
428
|
+
"original_size_bytes": len(blob_content),
|
|
429
|
+
"decoded_size_bytes": len(content_bytes),
|
|
430
|
+
"is_placeholder": False,
|
|
431
|
+
}
|
|
432
|
+
log.debug(
|
|
433
|
+
"%s Resource content item %d: decoded base64 blob content, original=%d bytes, decoded=%d bytes",
|
|
434
|
+
self.log_identifier,
|
|
435
|
+
index,
|
|
436
|
+
len(blob_content),
|
|
437
|
+
len(content_bytes),
|
|
438
|
+
)
|
|
439
|
+
except Exception as e:
|
|
440
|
+
log.error(
|
|
441
|
+
"%s Resource content item %d: failed to decode blob as base64: %s",
|
|
442
|
+
self.log_identifier,
|
|
443
|
+
index,
|
|
444
|
+
str(e),
|
|
445
|
+
)
|
|
446
|
+
return None # Fail processing for this item
|
|
447
|
+
elif text_content:
|
|
448
|
+
# Handle text content
|
|
449
|
+
content_bytes = text_content.encode("utf-8")
|
|
450
|
+
specific_metadata = {
|
|
451
|
+
"resource_uri": uri,
|
|
452
|
+
"has_text_content": True,
|
|
453
|
+
"has_blob_content": False,
|
|
454
|
+
"is_text_based": True,
|
|
455
|
+
"is_placeholder": False,
|
|
456
|
+
}
|
|
457
|
+
else:
|
|
458
|
+
# No content - create placeholder
|
|
459
|
+
content_bytes = f"Resource reference: {uri}".encode("utf-8")
|
|
460
|
+
specific_metadata = {
|
|
461
|
+
"resource_uri": uri,
|
|
462
|
+
"has_text_content": False,
|
|
463
|
+
"has_blob_content": False,
|
|
464
|
+
"is_text_based": is_text_based,
|
|
465
|
+
"is_placeholder": True,
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
log.debug(
|
|
469
|
+
"%s Processed resource content item %d: uri=%s, mime_type=%s, is_text_based=%s, size=%d bytes",
|
|
470
|
+
self.log_identifier,
|
|
471
|
+
index,
|
|
472
|
+
uri,
|
|
473
|
+
mime_type,
|
|
474
|
+
is_text_based,
|
|
475
|
+
len(content_bytes),
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
return self._create_content_item(
|
|
479
|
+
content_type=MCPContentType.RESOURCE,
|
|
480
|
+
content_bytes=content_bytes,
|
|
481
|
+
mime_type=mime_type,
|
|
482
|
+
filename=filename,
|
|
483
|
+
index=index,
|
|
484
|
+
specific_metadata=specific_metadata,
|
|
485
|
+
original_content=content_item,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
def _detect_and_parse_text_format(self, text_content: str) -> Tuple[str, bool, Any]:
|
|
489
|
+
"""
|
|
490
|
+
Detect text format and attempt to parse it.
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Tuple of (detected_format, parse_success, parsed_data)
|
|
494
|
+
"""
|
|
495
|
+
|
|
496
|
+
# Try JSON first
|
|
497
|
+
try:
|
|
498
|
+
# Only consider it JSON if it's a structured type (dict or list)
|
|
499
|
+
parsed_data = json.loads(text_content)
|
|
500
|
+
if isinstance(parsed_data, (dict, list)):
|
|
501
|
+
return TextFormat.JSON, True, parsed_data
|
|
502
|
+
except (json.JSONDecodeError, ValueError):
|
|
503
|
+
pass
|
|
504
|
+
|
|
505
|
+
# Try YAML
|
|
506
|
+
try:
|
|
507
|
+
parsed_data = yaml.safe_load(text_content)
|
|
508
|
+
# Only consider it YAML if it's not just a plain string
|
|
509
|
+
if isinstance(parsed_data, (dict, list)):
|
|
510
|
+
return TextFormat.YAML, True, parsed_data
|
|
511
|
+
except (yaml.YAMLError, ValueError):
|
|
512
|
+
pass
|
|
513
|
+
|
|
514
|
+
# Try CSV
|
|
515
|
+
try:
|
|
516
|
+
# Unescape newlines for robust CSV detection and parsing
|
|
517
|
+
processed_text_for_csv = text_content.replace("\\n", "\n")
|
|
518
|
+
# Check if it looks like CSV (has commas and multiple lines)
|
|
519
|
+
if "," in processed_text_for_csv and "\n" in processed_text_for_csv:
|
|
520
|
+
csv_reader = csv.reader(StringIO(processed_text_for_csv))
|
|
521
|
+
rows = list(csv_reader)
|
|
522
|
+
if len(rows) > 1 and len(rows[0]) > 1: # At least 2 rows and 2 columns
|
|
523
|
+
return TextFormat.CSV, True, rows
|
|
524
|
+
except Exception:
|
|
525
|
+
pass
|
|
526
|
+
|
|
527
|
+
# Check for Markdown indicators
|
|
528
|
+
markdown_indicators = ["#", "##", "###", "**", "*", "`", "```", "[", "]("]
|
|
529
|
+
if any(indicator in text_content for indicator in markdown_indicators):
|
|
530
|
+
return TextFormat.MARKDOWN, True, None
|
|
531
|
+
|
|
532
|
+
# Default to plain text
|
|
533
|
+
return TextFormat.PLAIN, True, None
|
|
534
|
+
|
|
535
|
+
def _get_text_mime_type_and_extension(
|
|
536
|
+
self, detected_format: str
|
|
537
|
+
) -> Tuple[str, str]:
|
|
538
|
+
"""Get MIME type and file extension for detected text format."""
|
|
539
|
+
|
|
540
|
+
format_mapping = {
|
|
541
|
+
TextFormat.JSON: ("application/json", ".json"),
|
|
542
|
+
TextFormat.YAML: ("application/x-yaml", ".yaml"),
|
|
543
|
+
TextFormat.CSV: ("text/csv", ".csv"),
|
|
544
|
+
TextFormat.MARKDOWN: ("text/markdown", ".md"),
|
|
545
|
+
TextFormat.PLAIN: ("text/plain", ".txt"),
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return format_mapping.get(detected_format, ("text/plain", ".txt"))
|
|
549
|
+
|
|
550
|
+
def _generate_text_filename(
|
|
551
|
+
self, detected_format: str, extension: str, index: int
|
|
552
|
+
) -> str:
|
|
553
|
+
"""Generate filename for text content based on format."""
|
|
554
|
+
|
|
555
|
+
format_prefix = {
|
|
556
|
+
TextFormat.JSON: "json",
|
|
557
|
+
TextFormat.YAML: "yaml",
|
|
558
|
+
TextFormat.CSV: "csv",
|
|
559
|
+
TextFormat.MARKDOWN: "markdown",
|
|
560
|
+
TextFormat.PLAIN: "text",
|
|
561
|
+
}.get(detected_format, "text")
|
|
562
|
+
|
|
563
|
+
return f"{self.tool_name}_{format_prefix}_{index}_{uuid.uuid4().hex[:8]}{extension}"
|
|
564
|
+
|
|
565
|
+
def _create_parsed_data_summary(
|
|
566
|
+
self, parsed_data: Any, detected_format: str
|
|
567
|
+
) -> Dict[str, Any]:
|
|
568
|
+
"""Create a summary of parsed data for metadata."""
|
|
569
|
+
|
|
570
|
+
summary = {"format": detected_format}
|
|
571
|
+
|
|
572
|
+
if detected_format == TextFormat.JSON:
|
|
573
|
+
if isinstance(parsed_data, dict):
|
|
574
|
+
summary["type"] = "object"
|
|
575
|
+
summary["keys"] = list(parsed_data.keys())[:10] # First 10 keys
|
|
576
|
+
summary["key_count"] = len(parsed_data)
|
|
577
|
+
elif isinstance(parsed_data, list):
|
|
578
|
+
summary["type"] = "array"
|
|
579
|
+
summary["length"] = len(parsed_data)
|
|
580
|
+
if parsed_data and isinstance(parsed_data[0], dict):
|
|
581
|
+
summary["first_item_keys"] = list(parsed_data[0].keys())[:5]
|
|
582
|
+
|
|
583
|
+
elif detected_format == TextFormat.YAML:
|
|
584
|
+
if isinstance(parsed_data, dict):
|
|
585
|
+
summary["type"] = "object"
|
|
586
|
+
summary["keys"] = list(parsed_data.keys())[:10]
|
|
587
|
+
summary["key_count"] = len(parsed_data)
|
|
588
|
+
elif isinstance(parsed_data, list):
|
|
589
|
+
summary["type"] = "array"
|
|
590
|
+
summary["length"] = len(parsed_data)
|
|
591
|
+
|
|
592
|
+
elif detected_format == TextFormat.CSV:
|
|
593
|
+
if isinstance(parsed_data, list) and parsed_data:
|
|
594
|
+
summary["type"] = "table"
|
|
595
|
+
summary["rows"] = len(parsed_data)
|
|
596
|
+
summary["columns"] = len(parsed_data[0]) if parsed_data[0] else 0
|
|
597
|
+
summary["headers"] = parsed_data[0][:5] if parsed_data[0] else []
|
|
598
|
+
|
|
599
|
+
return summary
|
|
600
|
+
|
|
601
|
+
def _extract_filename_from_uri(self, uri: str, mime_type: str, index: int) -> str:
|
|
602
|
+
"""Extract filename from URI or generate one based on URI components."""
|
|
603
|
+
try:
|
|
604
|
+
parsed_uri = urlparse(uri)
|
|
605
|
+
|
|
606
|
+
# Try to get filename from path
|
|
607
|
+
if parsed_uri.path:
|
|
608
|
+
path_parts = parsed_uri.path.strip("/").split("/")
|
|
609
|
+
if path_parts and path_parts[-1]:
|
|
610
|
+
filename_part = path_parts[-1]
|
|
611
|
+
# Clean filename and ensure it has an extension
|
|
612
|
+
clean_filename = re.sub(r'[<>:"/\\|?*]', "_", filename_part)
|
|
613
|
+
if "." not in clean_filename:
|
|
614
|
+
extension = get_extension_for_mime_type(mime_type)
|
|
615
|
+
clean_filename += extension
|
|
616
|
+
return f"{self.tool_name}_resource_{index}_{clean_filename}"
|
|
617
|
+
|
|
618
|
+
# Use hostname if available
|
|
619
|
+
if parsed_uri.hostname:
|
|
620
|
+
hostname = re.sub(r'[<>:"/\\|?*.]', "_", parsed_uri.hostname)
|
|
621
|
+
extension = get_extension_for_mime_type(mime_type)
|
|
622
|
+
return f"{self.tool_name}_resource_{index}_{hostname}{extension}"
|
|
623
|
+
|
|
624
|
+
except Exception as e:
|
|
625
|
+
log.debug("%s Error parsing URI %s: %s", self.log_identifier, uri, e)
|
|
626
|
+
|
|
627
|
+
# Fallback to generic filename
|
|
628
|
+
extension = get_extension_for_mime_type(mime_type)
|
|
629
|
+
return f"{self.tool_name}_resource_{index}_{uuid.uuid4().hex[:8]}{extension}"
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class MCPContentProcessorConfig:
|
|
633
|
+
"""Configuration for MCP content processing."""
|
|
634
|
+
|
|
635
|
+
def __init__(
|
|
636
|
+
self,
|
|
637
|
+
enable_intelligent_processing: bool = True,
|
|
638
|
+
enable_text_format_detection: bool = True,
|
|
639
|
+
enable_content_parsing: bool = True,
|
|
640
|
+
fallback_to_raw_on_error: bool = True,
|
|
641
|
+
max_content_items: int = 50,
|
|
642
|
+
max_single_item_size_mb: int = 100,
|
|
643
|
+
):
|
|
644
|
+
self.enable_intelligent_processing = enable_intelligent_processing
|
|
645
|
+
self.enable_text_format_detection = enable_text_format_detection
|
|
646
|
+
self.enable_content_parsing = enable_content_parsing
|
|
647
|
+
self.fallback_to_raw_on_error = fallback_to_raw_on_error
|
|
648
|
+
self.max_content_items = max_content_items
|
|
649
|
+
self.max_single_item_size_mb = max_single_item_size_mb
|
|
650
|
+
|
|
651
|
+
@classmethod
|
|
652
|
+
def from_dict(cls, config_dict: Dict[str, Any]) -> "MCPContentProcessorConfig":
|
|
653
|
+
"""Create config from dictionary."""
|
|
654
|
+
return cls(
|
|
655
|
+
enable_intelligent_processing=config_dict.get(
|
|
656
|
+
"enable_intelligent_processing", True
|
|
657
|
+
),
|
|
658
|
+
enable_text_format_detection=config_dict.get(
|
|
659
|
+
"enable_text_format_detection", True
|
|
660
|
+
),
|
|
661
|
+
enable_content_parsing=config_dict.get("enable_content_parsing", True),
|
|
662
|
+
fallback_to_raw_on_error=config_dict.get("fallback_to_raw_on_error", True),
|
|
663
|
+
max_content_items=config_dict.get("max_content_items", 50),
|
|
664
|
+
max_single_item_size_mb=config_dict.get("max_single_item_size_mb", 100),
|
|
665
|
+
)
|