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.

Files changed (141) hide show
  1. solace_agent_mesh/agent/adk/artifacts/__init__.py +1 -0
  2. solace_agent_mesh/agent/adk/{filesystem_artifact_service.py → artifacts/filesystem_artifact_service.py} +14 -15
  3. solace_agent_mesh/agent/adk/artifacts/s3_artifact_service.py +440 -0
  4. solace_agent_mesh/agent/adk/callbacks.py +123 -159
  5. solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +316 -0
  6. solace_agent_mesh/agent/adk/intelligent_mcp_callbacks.py +414 -0
  7. solace_agent_mesh/agent/adk/mcp_content_processor.py +665 -0
  8. solace_agent_mesh/agent/adk/services.py +43 -1
  9. solace_agent_mesh/agent/adk/setup.py +85 -45
  10. solace_agent_mesh/agent/adk/tool_wrapper.py +19 -3
  11. solace_agent_mesh/agent/protocol/event_handlers.py +1 -1
  12. solace_agent_mesh/agent/sac/app.py +67 -0
  13. solace_agent_mesh/agent/sac/component.py +14 -86
  14. solace_agent_mesh/assets/docs/404.html +3 -3
  15. solace_agent_mesh/assets/docs/assets/js/04989206.b9dfe831.js +1 -0
  16. solace_agent_mesh/assets/docs/assets/js/0e682baa.b3bbde9a.js +1 -0
  17. solace_agent_mesh/assets/docs/assets/js/1023fc19.364235d5.js +1 -0
  18. solace_agent_mesh/assets/docs/assets/js/1523c6b4.1b0ec6f9.js +1 -0
  19. solace_agent_mesh/assets/docs/assets/js/166ab619.e8f3a7c7.js +1 -0
  20. solace_agent_mesh/assets/docs/assets/js/21ceee5f.3bf39250.js +1 -0
  21. solace_agent_mesh/assets/docs/assets/js/3d406171.7d02a73b.js +1 -0
  22. solace_agent_mesh/assets/docs/assets/js/42b3f8d8.8ccb9901.js +1 -0
  23. solace_agent_mesh/assets/docs/assets/js/442a8107.b3159bb2.js +1 -0
  24. solace_agent_mesh/assets/docs/assets/js/4c2787c2.fc6804f2.js +1 -0
  25. solace_agent_mesh/assets/docs/assets/js/5b4258a4.0d080cd9.js +1 -0
  26. solace_agent_mesh/assets/docs/assets/js/75384d09.ccd480c4.js +1 -0
  27. solace_agent_mesh/assets/docs/assets/js/768e31b0.8b51cd70.js +1 -0
  28. solace_agent_mesh/assets/docs/assets/js/945fb41e.c63791d1.js +1 -0
  29. solace_agent_mesh/assets/docs/assets/js/{9eff14a2.036c35ea.js → 9eff14a2.472b0310.js} +1 -1
  30. solace_agent_mesh/assets/docs/assets/js/a3a92b25.4b7fa6a2.js +1 -0
  31. solace_agent_mesh/assets/docs/assets/js/aba87c2f.76376d7c.js +1 -0
  32. solace_agent_mesh/assets/docs/assets/js/ae4415af.7a2f0bbf.js +1 -0
  33. solace_agent_mesh/assets/docs/assets/js/b7006a3a.73a79653.js +1 -0
  34. solace_agent_mesh/assets/docs/assets/js/beecea0d.ae31f6a7.js +1 -0
  35. solace_agent_mesh/assets/docs/assets/js/c2c06897.587b4af5.js +1 -0
  36. solace_agent_mesh/assets/docs/assets/js/{cd3d4052.ca6eed8c.js → cd3d4052.b6535013.js} +1 -1
  37. solace_agent_mesh/assets/docs/assets/js/f284c35a.731836ad.js +1 -0
  38. solace_agent_mesh/assets/docs/assets/js/f897a61a.0aa29dbb.js +1 -0
  39. solace_agent_mesh/assets/docs/assets/js/main.6dba4a66.js +2 -0
  40. solace_agent_mesh/assets/docs/assets/js/runtime~main.6415ad00.js +1 -0
  41. solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +28 -4
  42. solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +6 -6
  43. solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +8 -8
  44. solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +5 -5
  45. solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +5 -5
  46. solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +34 -5
  47. solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +4 -4
  48. solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +5 -5
  49. solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +4 -4
  50. solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +6 -6
  51. solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +72 -0
  52. solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +5 -5
  53. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +7 -7
  54. solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +35 -16
  55. solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +4 -4
  56. solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +17 -11
  57. solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +4 -4
  58. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +5 -5
  59. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +4 -4
  60. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +6 -6
  61. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +6 -6
  62. solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +6 -6
  63. solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +4 -4
  64. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +8 -8
  65. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +14 -14
  66. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +8 -8
  67. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +4 -4
  68. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +6 -6
  69. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +35 -23
  70. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +4 -4
  71. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +6 -6
  72. solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +4 -4
  73. solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +4 -4
  74. solace_agent_mesh/assets/docs/lunr-index-1756153049706.json +1 -0
  75. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  76. solace_agent_mesh/assets/docs/search-doc-1756153049706.json +1 -0
  77. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  78. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  79. solace_agent_mesh/cli/__init__.py +1 -1
  80. solace_agent_mesh/cli/commands/add_cmd/add_cmd_llm.txt +1 -1
  81. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +67 -10
  82. solace_agent_mesh/cli/commands/add_cmd/gateway_cmd.py +2 -2
  83. solace_agent_mesh/cli/commands/eval_cmd.py +8 -2
  84. solace_agent_mesh/cli/commands/init_cmd/__init__.py +20 -2
  85. solace_agent_mesh/cli/commands/init_cmd/env_step.py +25 -1
  86. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +45 -1
  87. solace_agent_mesh/cli/utils.py +21 -12
  88. solace_agent_mesh/client/webui/frontend/static/assets/main-BucUdn9m.js +673 -0
  89. solace_agent_mesh/client/webui/frontend/static/index.html +1 -1
  90. solace_agent_mesh/common/a2a_protocol.py +1 -1
  91. solace_agent_mesh/common/utils/mime_helpers.py +60 -1
  92. solace_agent_mesh/config_portal/backend/server.py +1 -1
  93. solace_agent_mesh/config_portal/frontend/static/client/assets/{_index-xSu2leR8.js → _index-MqsrTd6g.js} +9 -9
  94. solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-950eb3be.js → manifest-28271392.js} +1 -1
  95. solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
  96. solace_agent_mesh/core_a2a/core_a2a_llm.txt +1 -1
  97. solace_agent_mesh/core_a2a/service.py +1 -1
  98. solace_agent_mesh/evaluation/run.py +149 -15
  99. solace_agent_mesh/evaluation/summary_builder.py +5 -3
  100. solace_agent_mesh/gateway/http_sse/dependencies.py +1 -1
  101. solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +1 -1
  102. solace_agent_mesh/gateway/http_sse/services/task_service.py +1 -1
  103. solace_agent_mesh/llm_detail.txt +2 -2
  104. solace_agent_mesh/templates/agent_template.yaml +1 -1
  105. solace_agent_mesh/templates/plugin_agent_config_template.yaml +3 -3
  106. solace_agent_mesh/templates/plugin_readme_template.md +1 -1
  107. solace_agent_mesh/templates/shared_config.yaml +8 -1
  108. {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.8.dist-info}/METADATA +4 -1
  109. {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.8.dist-info}/RECORD +113 -108
  110. solace_agent_mesh/assets/docs/assets/js/04989206.da8246cd.js +0 -1
  111. solace_agent_mesh/assets/docs/assets/js/0e682baa.79f0ab22.js +0 -1
  112. solace_agent_mesh/assets/docs/assets/js/1023fc19.8e6d174c.js +0 -1
  113. solace_agent_mesh/assets/docs/assets/js/1523c6b4.91c7bc01.js +0 -1
  114. solace_agent_mesh/assets/docs/assets/js/166ab619.7d97ccaf.js +0 -1
  115. solace_agent_mesh/assets/docs/assets/js/21ceee5f.614fa8dd.js +0 -1
  116. solace_agent_mesh/assets/docs/assets/js/3d406171.9b081d5f.js +0 -1
  117. solace_agent_mesh/assets/docs/assets/js/42b3f8d8.36090198.js +0 -1
  118. solace_agent_mesh/assets/docs/assets/js/442a8107.5ba94b65.js +0 -1
  119. solace_agent_mesh/assets/docs/assets/js/4c2787c2.66ee00e9.js +0 -1
  120. solace_agent_mesh/assets/docs/assets/js/5b4258a4.bda20761.js +0 -1
  121. solace_agent_mesh/assets/docs/assets/js/75384d09.c3991823.js +0 -1
  122. solace_agent_mesh/assets/docs/assets/js/768e31b0.a12673db.js +0 -1
  123. solace_agent_mesh/assets/docs/assets/js/945fb41e.74d728aa.js +0 -1
  124. solace_agent_mesh/assets/docs/assets/js/a3a92b25.26ca071f.js +0 -1
  125. solace_agent_mesh/assets/docs/assets/js/aba87c2f.a6b84da6.js +0 -1
  126. solace_agent_mesh/assets/docs/assets/js/ae4415af.96189a93.js +0 -1
  127. solace_agent_mesh/assets/docs/assets/js/b7006a3a.38c0cf3d.js +0 -1
  128. solace_agent_mesh/assets/docs/assets/js/bb2ef573.56931473.js +0 -1
  129. solace_agent_mesh/assets/docs/assets/js/c2c06897.63b76e9e.js +0 -1
  130. solace_agent_mesh/assets/docs/assets/js/f284c35a.5aff74ab.js +0 -1
  131. solace_agent_mesh/assets/docs/assets/js/f897a61a.862b0514.js +0 -1
  132. solace_agent_mesh/assets/docs/assets/js/main.ea9672b6.js +0 -2
  133. solace_agent_mesh/assets/docs/assets/js/runtime~main.aa687c82.js +0 -1
  134. solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +0 -17
  135. solace_agent_mesh/assets/docs/lunr-index-1755285974624.json +0 -1
  136. solace_agent_mesh/assets/docs/search-doc-1755285974624.json +0 -1
  137. solace_agent_mesh/client/webui/frontend/static/assets/main-DzKPMTRs.js +0 -673
  138. /solace_agent_mesh/assets/docs/assets/js/{main.ea9672b6.js.LICENSE.txt → main.6dba4a66.js.LICENSE.txt} +0 -0
  139. {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.8.dist-info}/WHEEL +0 -0
  140. {solace_agent_mesh-1.0.6.dist-info → solace_agent_mesh-1.0.8.dist-info}/entry_points.txt +0 -0
  141. {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
+ )