python-alfresco-mcp-server 1.1.0__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.
- alfresco_mcp_server/__init__.py +27 -0
- alfresco_mcp_server/config.py +104 -0
- alfresco_mcp_server/fastmcp_server.py +259 -0
- alfresco_mcp_server/prompts/__init__.py +7 -0
- alfresco_mcp_server/prompts/search_and_analyze.py +67 -0
- alfresco_mcp_server/resources/__init__.py +7 -0
- alfresco_mcp_server/resources/repository_resources.py +205 -0
- alfresco_mcp_server/tools/__init__.py +13 -0
- alfresco_mcp_server/tools/core/__init__.py +27 -0
- alfresco_mcp_server/tools/core/browse_repository.py +164 -0
- alfresco_mcp_server/tools/core/cancel_checkout.py +160 -0
- alfresco_mcp_server/tools/core/checkin_document.py +264 -0
- alfresco_mcp_server/tools/core/checkout_document.py +258 -0
- alfresco_mcp_server/tools/core/create_folder.py +105 -0
- alfresco_mcp_server/tools/core/delete_node.py +88 -0
- alfresco_mcp_server/tools/core/download_document.py +214 -0
- alfresco_mcp_server/tools/core/get_node_properties.py +195 -0
- alfresco_mcp_server/tools/core/update_node_properties.py +138 -0
- alfresco_mcp_server/tools/core/upload_document.py +244 -0
- alfresco_mcp_server/tools/search/__init__.py +15 -0
- alfresco_mcp_server/tools/search/advanced_search.py +204 -0
- alfresco_mcp_server/tools/search/cmis_search.py +180 -0
- alfresco_mcp_server/tools/search/search_by_metadata.py +183 -0
- alfresco_mcp_server/tools/search/search_content.py +186 -0
- alfresco_mcp_server/utils/__init__.py +34 -0
- alfresco_mcp_server/utils/connection.py +114 -0
- alfresco_mcp_server/utils/file_type_analysis.py +163 -0
- alfresco_mcp_server/utils/json_utils.py +135 -0
- python_alfresco_mcp_server-1.1.0.dist-info/METADATA +631 -0
- python_alfresco_mcp_server-1.1.0.dist-info/RECORD +34 -0
- python_alfresco_mcp_server-1.1.0.dist-info/WHEEL +5 -0
- python_alfresco_mcp_server-1.1.0.dist-info/entry_points.txt +2 -0
- python_alfresco_mcp_server-1.1.0.dist-info/licenses/LICENSE +201 -0
- python_alfresco_mcp_server-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Advanced search tool for Alfresco MCP Server.
|
|
3
|
+
Each tool is self-contained with its own validation, business logic, and env handling.
|
|
4
|
+
"""
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from fastmcp import Context
|
|
8
|
+
|
|
9
|
+
from ...utils.connection import ensure_connection
|
|
10
|
+
from ...utils.json_utils import safe_format_output
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def advanced_search_impl(
|
|
16
|
+
query: str,
|
|
17
|
+
sort_field: str = "cm:modified",
|
|
18
|
+
sort_ascending: bool = False,
|
|
19
|
+
max_results: int = 25,
|
|
20
|
+
ctx: Optional[Context] = None
|
|
21
|
+
) -> str:
|
|
22
|
+
"""Advanced search with sorting and filtering capabilities.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
query: Search query string (supports Alfresco Full Text Search syntax)
|
|
26
|
+
sort_field: Field to sort by (default: cm:modified)
|
|
27
|
+
sort_ascending: Sort order (default: False for descending)
|
|
28
|
+
max_results: Maximum number of results to return (default: 25)
|
|
29
|
+
ctx: MCP context for progress reporting
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Formatted search results with metadata, sorted as requested
|
|
33
|
+
"""
|
|
34
|
+
# Parameter validation and extraction
|
|
35
|
+
try:
|
|
36
|
+
# Extract parameters with fallback handling
|
|
37
|
+
if hasattr(query, 'value'):
|
|
38
|
+
actual_query = str(query.value)
|
|
39
|
+
else:
|
|
40
|
+
actual_query = str(query)
|
|
41
|
+
|
|
42
|
+
if hasattr(sort_field, 'value'):
|
|
43
|
+
actual_sort_field = str(sort_field.value)
|
|
44
|
+
else:
|
|
45
|
+
actual_sort_field = str(sort_field)
|
|
46
|
+
|
|
47
|
+
if hasattr(sort_ascending, 'value'):
|
|
48
|
+
actual_sort_ascending = bool(sort_ascending.value)
|
|
49
|
+
else:
|
|
50
|
+
actual_sort_ascending = bool(sort_ascending)
|
|
51
|
+
|
|
52
|
+
if hasattr(max_results, 'value'):
|
|
53
|
+
actual_max_results = int(max_results.value)
|
|
54
|
+
else:
|
|
55
|
+
actual_max_results = int(max_results)
|
|
56
|
+
|
|
57
|
+
# Clean and normalize for display (preserve Unicode characters)
|
|
58
|
+
safe_query_display = str(actual_query)
|
|
59
|
+
safe_sort_field_display = str(actual_sort_field)
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.error(f"Parameter extraction error: {e}")
|
|
63
|
+
return f"ERROR: Parameter error: {str(e)}"
|
|
64
|
+
|
|
65
|
+
if ctx:
|
|
66
|
+
await ctx.info(safe_format_output(f"Advanced search for '{safe_query_display}' with sorting..."))
|
|
67
|
+
await ctx.report_progress(0.0)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
# Get all clients that ensure_connection() already created
|
|
71
|
+
master_client = await ensure_connection()
|
|
72
|
+
|
|
73
|
+
# Import search_utils
|
|
74
|
+
from python_alfresco_api.utils import search_utils
|
|
75
|
+
|
|
76
|
+
# Access the search client that was already created
|
|
77
|
+
search_client = master_client.search
|
|
78
|
+
|
|
79
|
+
logger.debug(f"Advanced search for: '{safe_query_display}', sort: {safe_sort_field_display} ({'asc' if actual_sort_ascending else 'desc'})")
|
|
80
|
+
|
|
81
|
+
if ctx:
|
|
82
|
+
await ctx.report_progress(0.3)
|
|
83
|
+
|
|
84
|
+
# Use search_utils.advanced_search() utility with existing search_client
|
|
85
|
+
if ctx:
|
|
86
|
+
await ctx.report_progress(0.5)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
# Use search_utils.advanced_search() with existing search_client that has working authentication
|
|
90
|
+
search_results = search_utils.advanced_search(
|
|
91
|
+
search_client,
|
|
92
|
+
actual_query,
|
|
93
|
+
max_items=actual_max_results,
|
|
94
|
+
sort_by=actual_sort_field,
|
|
95
|
+
sort_ascending=actual_sort_ascending
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if not search_results:
|
|
99
|
+
logger.debug("Advanced search returned None, attempting fallback to simple search")
|
|
100
|
+
search_results = search_utils.simple_search(search_client, actual_query, max_items=actual_max_results)
|
|
101
|
+
|
|
102
|
+
# Check for different possible SearchResult structures
|
|
103
|
+
if not search_results:
|
|
104
|
+
logger.error(f"No search results returned")
|
|
105
|
+
return safe_format_output(f"ERROR: Advanced search failed - no results returned")
|
|
106
|
+
|
|
107
|
+
# Try to get entries from different possible structures
|
|
108
|
+
entries = []
|
|
109
|
+
|
|
110
|
+
if hasattr(search_results, 'list') and search_results.list and hasattr(search_results.list, 'entries'):
|
|
111
|
+
entries = search_results.list.entries if search_results.list else []
|
|
112
|
+
logger.debug(f"Found entries using list attribute: {len(entries)}")
|
|
113
|
+
elif hasattr(search_results, 'list_') and search_results.list_ and hasattr(search_results.list_, 'entries'):
|
|
114
|
+
entries = search_results.list_.entries if search_results.list_ else []
|
|
115
|
+
logger.debug(f"Found entries using list_ attribute: {len(entries)}")
|
|
116
|
+
elif hasattr(search_results, 'entries'):
|
|
117
|
+
entries = search_results.entries
|
|
118
|
+
logger.debug(f"Found entries using direct entries attribute: {len(entries)}")
|
|
119
|
+
elif hasattr(search_results, 'results'):
|
|
120
|
+
entries = search_results.results
|
|
121
|
+
logger.debug(f"Found entries using results attribute: {len(entries)}")
|
|
122
|
+
else:
|
|
123
|
+
logger.error(f"SearchResult structure not recognized")
|
|
124
|
+
return safe_format_output(f"ERROR: Advanced search failed - unknown SearchResult structure")
|
|
125
|
+
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.error(f"Advanced search failed: {e}")
|
|
128
|
+
# Try fallback to simple search
|
|
129
|
+
try:
|
|
130
|
+
logger.debug("Attempting fallback to simple search after advanced search error")
|
|
131
|
+
search_results = search_utils.simple_search(search_client, actual_query, max_items=actual_max_results)
|
|
132
|
+
if not search_results:
|
|
133
|
+
return safe_format_output(f"ERROR: Both advanced and simple search failed: {str(e)}")
|
|
134
|
+
# Extract entries from simple search result
|
|
135
|
+
entries = []
|
|
136
|
+
if hasattr(search_results, 'list_') and search_results.list_ and hasattr(search_results.list_, 'entries'):
|
|
137
|
+
entries = search_results.list_.entries if search_results.list_ else []
|
|
138
|
+
logger.debug(f"Fallback simple search found {len(entries)} results")
|
|
139
|
+
else:
|
|
140
|
+
return safe_format_output(f"ERROR: Both advanced and simple search failed: {str(e)}")
|
|
141
|
+
except Exception as fallback_error:
|
|
142
|
+
logger.error(f"Fallback simple search also failed: {fallback_error}")
|
|
143
|
+
return safe_format_output(f"ERROR: Advanced search failed: {str(e)}")
|
|
144
|
+
|
|
145
|
+
if ctx:
|
|
146
|
+
await ctx.report_progress(1.0)
|
|
147
|
+
|
|
148
|
+
# Process final results
|
|
149
|
+
if entries:
|
|
150
|
+
logger.info(f"Found {len(entries)} search results")
|
|
151
|
+
result_text = f"Found {len(entries)} item(s) matching '{safe_query_display}':\n\n"
|
|
152
|
+
|
|
153
|
+
for i, entry in enumerate(entries, 1):
|
|
154
|
+
# Handle different possible entry structures
|
|
155
|
+
node = None
|
|
156
|
+
if isinstance(entry, dict):
|
|
157
|
+
if 'entry' in entry:
|
|
158
|
+
node = entry['entry']
|
|
159
|
+
elif 'name' in entry: # Direct node structure
|
|
160
|
+
node = entry
|
|
161
|
+
else:
|
|
162
|
+
logger.debug(f"Unknown entry structure: {entry}")
|
|
163
|
+
continue
|
|
164
|
+
elif hasattr(entry, 'entry'): # ResultSetRowEntry object
|
|
165
|
+
node = entry.entry
|
|
166
|
+
else:
|
|
167
|
+
logger.debug(f"Entry is not a dict or ResultSetRowEntry: {type(entry)}")
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
if node:
|
|
171
|
+
# Handle both dict and ResultNode objects
|
|
172
|
+
if isinstance(node, dict):
|
|
173
|
+
name = str(node.get('name', 'Unknown'))
|
|
174
|
+
node_id = str(node.get('id', 'Unknown'))
|
|
175
|
+
node_type_actual = str(node.get('nodeType', 'Unknown'))
|
|
176
|
+
created_at = str(node.get('createdAt', 'Unknown'))
|
|
177
|
+
else:
|
|
178
|
+
# ResultNode object - access attributes directly
|
|
179
|
+
name = str(getattr(node, 'name', 'Unknown'))
|
|
180
|
+
node_id = str(getattr(node, 'id', 'Unknown'))
|
|
181
|
+
node_type_actual = str(getattr(node, 'node_type', 'Unknown'))
|
|
182
|
+
created_at = str(getattr(node, 'created_at', 'Unknown'))
|
|
183
|
+
|
|
184
|
+
# Apply safe formatting to individual fields to prevent emoji encoding issues
|
|
185
|
+
safe_name = safe_format_output(name)
|
|
186
|
+
safe_node_id = safe_format_output(node_id)
|
|
187
|
+
safe_node_type = safe_format_output(node_type_actual)
|
|
188
|
+
safe_created_at = safe_format_output(created_at)
|
|
189
|
+
|
|
190
|
+
result_text += f"{i}. {safe_name}\n"
|
|
191
|
+
result_text += f" - ID: {safe_node_id}\n"
|
|
192
|
+
result_text += f" - Type: {safe_node_type}\n"
|
|
193
|
+
result_text += f" - Created: {safe_created_at}\n\n"
|
|
194
|
+
|
|
195
|
+
return safe_format_output(result_text)
|
|
196
|
+
else:
|
|
197
|
+
# Simple "0" for zero results as requested
|
|
198
|
+
return "0"
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
error_msg = f"ERROR: Advanced search failed: {str(e)}"
|
|
202
|
+
if ctx:
|
|
203
|
+
await ctx.error(safe_format_output(error_msg))
|
|
204
|
+
return safe_format_output(error_msg)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CMIS search tool for Alfresco MCP Server.
|
|
3
|
+
Each tool is self-contained with its own validation, business logic, and env handling.
|
|
4
|
+
"""
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from fastmcp import Context
|
|
8
|
+
|
|
9
|
+
from ...utils.connection import ensure_connection
|
|
10
|
+
from ...utils.json_utils import safe_format_output
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def cmis_search_impl(
|
|
16
|
+
cmis_query: str,
|
|
17
|
+
max_results: int = 25,
|
|
18
|
+
ctx: Optional[Context] = None
|
|
19
|
+
) -> str:
|
|
20
|
+
"""Search using CMIS SQL syntax.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
cmis_query: CMIS SQL query string
|
|
24
|
+
max_results: Maximum number of results to return (default: 25)
|
|
25
|
+
ctx: MCP context for progress reporting
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Formatted search results from CMIS query
|
|
29
|
+
"""
|
|
30
|
+
# Parameter validation and extraction
|
|
31
|
+
try:
|
|
32
|
+
# Extract parameters with fallback handling
|
|
33
|
+
if hasattr(cmis_query, 'value'):
|
|
34
|
+
actual_query = str(cmis_query.value)
|
|
35
|
+
else:
|
|
36
|
+
actual_query = str(cmis_query)
|
|
37
|
+
|
|
38
|
+
if hasattr(max_results, 'value'):
|
|
39
|
+
actual_max_results = int(max_results.value)
|
|
40
|
+
else:
|
|
41
|
+
actual_max_results = int(max_results)
|
|
42
|
+
|
|
43
|
+
# Clean and normalize for display (preserve Unicode characters)
|
|
44
|
+
safe_query_display = str(actual_query)
|
|
45
|
+
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.error(f"Parameter extraction error: {e}")
|
|
48
|
+
return f"ERROR: Parameter error: {str(e)}"
|
|
49
|
+
|
|
50
|
+
if not actual_query.strip():
|
|
51
|
+
return """CMIS Search Tool
|
|
52
|
+
|
|
53
|
+
Usage: Provide a CMIS SQL query to search Alfresco repository.
|
|
54
|
+
|
|
55
|
+
Example CMIS queries:
|
|
56
|
+
- SELECT * FROM cmis:document WHERE cmis:name LIKE 'test%'
|
|
57
|
+
- SELECT * FROM cmis:folder WHERE CONTAINS('project')
|
|
58
|
+
- SELECT * FROM cmis:document WHERE cmis:creationDate > '2024-01-01T00:00:00.000Z'
|
|
59
|
+
- SELECT * FROM cmis:document WHERE cmis:contentStreamMimeType = 'application/pdf'
|
|
60
|
+
|
|
61
|
+
CMIS provides precise SQL queries for exact matching and filtering.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
if ctx:
|
|
65
|
+
await ctx.info(safe_format_output(f"CMIS search for: '{safe_query_display}'"))
|
|
66
|
+
await ctx.report_progress(0.0)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# Get all clients that ensure_connection() already created
|
|
70
|
+
master_client = await ensure_connection()
|
|
71
|
+
|
|
72
|
+
# Access the search client that was already created (same as other search tools)
|
|
73
|
+
search_client = master_client.search
|
|
74
|
+
|
|
75
|
+
logger.info(f"CMIS search for: '{safe_query_display}'")
|
|
76
|
+
|
|
77
|
+
if ctx:
|
|
78
|
+
await ctx.report_progress(0.3)
|
|
79
|
+
|
|
80
|
+
# Use same pattern as other search tools but with CMIS language
|
|
81
|
+
try:
|
|
82
|
+
# Import the SearchRequest model for CMIS queries
|
|
83
|
+
from python_alfresco_api.raw_clients.alfresco_search_client.search_client.models import SearchRequest, RequestQuery, RequestPagination, RequestQueryLanguage
|
|
84
|
+
from python_alfresco_api.raw_clients.alfresco_search_client.search_client.types import UNSET
|
|
85
|
+
|
|
86
|
+
# Create CMIS search request (same pattern as search_utils.simple_search but with CMIS language)
|
|
87
|
+
request_query = RequestQuery(
|
|
88
|
+
query=actual_query,
|
|
89
|
+
language=RequestQueryLanguage.CMIS # Use CMIS instead of AFTS
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
request_pagination = RequestPagination(
|
|
93
|
+
max_items=actual_max_results,
|
|
94
|
+
skip_count=0
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
search_request = SearchRequest(
|
|
98
|
+
query=request_query,
|
|
99
|
+
paging=request_pagination,
|
|
100
|
+
include=UNSET
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Use same pattern as search_utils.simple_search
|
|
104
|
+
search_results = search_client.search.search(search_request)
|
|
105
|
+
|
|
106
|
+
if search_results and hasattr(search_results, 'list_'):
|
|
107
|
+
entries_list = search_results.list_.entries if search_results.list_ else []
|
|
108
|
+
logger.info(f"Found {len(entries_list)} CMIS search results")
|
|
109
|
+
|
|
110
|
+
if ctx:
|
|
111
|
+
await ctx.report_progress(1.0)
|
|
112
|
+
|
|
113
|
+
if not entries_list:
|
|
114
|
+
return "0"
|
|
115
|
+
|
|
116
|
+
result_text = f"Found {len(entries_list)} item(s) matching the CMIS query:\n\n"
|
|
117
|
+
|
|
118
|
+
for i, entry in enumerate(entries_list, 1):
|
|
119
|
+
# Debug: Log the entry structure
|
|
120
|
+
logger.debug(f"Entry {i} type: {type(entry)}, content: {entry}")
|
|
121
|
+
|
|
122
|
+
# Handle different possible entry structures
|
|
123
|
+
node = None
|
|
124
|
+
if isinstance(entry, dict):
|
|
125
|
+
if 'entry' in entry:
|
|
126
|
+
node = entry['entry']
|
|
127
|
+
elif 'name' in entry: # Direct node structure
|
|
128
|
+
node = entry
|
|
129
|
+
else:
|
|
130
|
+
logger.warning(f"Unknown entry structure: {entry}")
|
|
131
|
+
continue
|
|
132
|
+
elif hasattr(entry, 'entry'): # ResultSetRowEntry object
|
|
133
|
+
node = entry.entry
|
|
134
|
+
else:
|
|
135
|
+
logger.warning(f"Entry is not a dict or ResultSetRowEntry: {type(entry)}")
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
if node:
|
|
139
|
+
# Handle both dict and ResultNode objects
|
|
140
|
+
if isinstance(node, dict):
|
|
141
|
+
name = str(node.get('name', 'Unknown'))
|
|
142
|
+
node_id = str(node.get('id', 'Unknown'))
|
|
143
|
+
node_type = str(node.get('nodeType', 'Unknown'))
|
|
144
|
+
created_at = str(node.get('createdAt', 'Unknown'))
|
|
145
|
+
else:
|
|
146
|
+
# ResultNode object - access attributes directly
|
|
147
|
+
name = str(getattr(node, 'name', 'Unknown'))
|
|
148
|
+
node_id = str(getattr(node, 'id', 'Unknown'))
|
|
149
|
+
node_type = str(getattr(node, 'node_type', 'Unknown'))
|
|
150
|
+
created_at = str(getattr(node, 'created_at', 'Unknown'))
|
|
151
|
+
|
|
152
|
+
# Clean JSON-friendly formatting (no markdown syntax)
|
|
153
|
+
# Apply safe formatting to individual fields to prevent emoji encoding issues
|
|
154
|
+
safe_name = safe_format_output(name)
|
|
155
|
+
safe_node_id = safe_format_output(node_id)
|
|
156
|
+
safe_node_type = safe_format_output(node_type)
|
|
157
|
+
safe_created_at = safe_format_output(created_at)
|
|
158
|
+
|
|
159
|
+
result_text += f"{i}. {safe_name}\n"
|
|
160
|
+
result_text += f" - ID: {safe_node_id}\n"
|
|
161
|
+
result_text += f" - Type: {safe_node_type}\n"
|
|
162
|
+
result_text += f" - Created: {safe_created_at}\n\n"
|
|
163
|
+
|
|
164
|
+
return safe_format_output(result_text)
|
|
165
|
+
else:
|
|
166
|
+
return safe_format_output(f"ERROR: CMIS search failed - invalid response from Alfresco")
|
|
167
|
+
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.error(f"CMIS search failed: {e}")
|
|
170
|
+
return safe_format_output(f"ERROR: CMIS search failed: {str(e)}")
|
|
171
|
+
|
|
172
|
+
except Exception as e:
|
|
173
|
+
# Preserve Unicode characters in error messages
|
|
174
|
+
error_msg = f"ERROR: CMIS search failed: {str(e)}"
|
|
175
|
+
if ctx:
|
|
176
|
+
await ctx.error(safe_format_output(error_msg))
|
|
177
|
+
return safe_format_output(error_msg)
|
|
178
|
+
|
|
179
|
+
if ctx:
|
|
180
|
+
await ctx.info(safe_format_output("CMIS search completed!"))
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Search by metadata tool for Alfresco MCP Server.
|
|
3
|
+
Each tool is self-contained with its own validation, business logic, and env handling.
|
|
4
|
+
"""
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from fastmcp import Context
|
|
8
|
+
|
|
9
|
+
from ...utils.connection import ensure_connection
|
|
10
|
+
from ...utils.json_utils import safe_format_output
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def search_by_metadata_impl(
|
|
16
|
+
term: str = "",
|
|
17
|
+
creator: str = "",
|
|
18
|
+
content_type: str = "",
|
|
19
|
+
max_results: int = 25,
|
|
20
|
+
ctx: Optional[Context] = None
|
|
21
|
+
) -> str:
|
|
22
|
+
"""Search for content in Alfresco by metadata fields.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
term: Search term to include (primary search parameter)
|
|
26
|
+
creator: Creator username to filter by
|
|
27
|
+
content_type: Content type to filter by (e.g., "cm:content", "cm:folder")
|
|
28
|
+
max_results: Maximum number of results to return (default: 25)
|
|
29
|
+
ctx: MCP context for progress reporting
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Formatted search results with metadata
|
|
33
|
+
"""
|
|
34
|
+
# Parameter validation and extraction
|
|
35
|
+
try:
|
|
36
|
+
# Extract parameters with fallback handling
|
|
37
|
+
if hasattr(creator, 'value'):
|
|
38
|
+
actual_creator = str(creator.value)
|
|
39
|
+
else:
|
|
40
|
+
actual_creator = str(creator)
|
|
41
|
+
|
|
42
|
+
if hasattr(content_type, 'value'):
|
|
43
|
+
actual_content_type = str(content_type.value)
|
|
44
|
+
else:
|
|
45
|
+
actual_content_type = str(content_type)
|
|
46
|
+
|
|
47
|
+
if hasattr(term, 'value'):
|
|
48
|
+
actual_term = str(term.value)
|
|
49
|
+
else:
|
|
50
|
+
actual_term = str(term)
|
|
51
|
+
|
|
52
|
+
if hasattr(max_results, 'value'):
|
|
53
|
+
actual_max_results = int(max_results.value)
|
|
54
|
+
else:
|
|
55
|
+
actual_max_results = int(max_results)
|
|
56
|
+
|
|
57
|
+
# Clean and normalize for display (preserve Unicode characters)
|
|
58
|
+
safe_creator_display = str(actual_creator)
|
|
59
|
+
safe_content_type_display = str(actual_content_type)
|
|
60
|
+
safe_term_display = str(actual_term)
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f"Parameter extraction error: {e}")
|
|
64
|
+
return f"ERROR: Parameter error: {str(e)}"
|
|
65
|
+
|
|
66
|
+
if ctx:
|
|
67
|
+
await ctx.info(safe_format_output(f"Searching by metadata in Alfresco..."))
|
|
68
|
+
await ctx.report_progress(0.0)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
# Get all clients that ensure_connection() already created
|
|
72
|
+
master_client = await ensure_connection()
|
|
73
|
+
|
|
74
|
+
# Import search_utils
|
|
75
|
+
from python_alfresco_api.utils import search_utils
|
|
76
|
+
|
|
77
|
+
# Access the search client that was already created
|
|
78
|
+
search_client = master_client.search
|
|
79
|
+
|
|
80
|
+
logger.info(f"Searching Alfresco by metadata - creator: '{safe_creator_display}', type: '{safe_content_type_display}', term: '{safe_term_display}'")
|
|
81
|
+
|
|
82
|
+
if ctx:
|
|
83
|
+
await ctx.report_progress(0.3)
|
|
84
|
+
|
|
85
|
+
# Build query using search_utils.build_query() utility
|
|
86
|
+
search_query = search_utils.build_query(
|
|
87
|
+
term=actual_term if actual_term.strip() else None,
|
|
88
|
+
content_type=actual_content_type if actual_content_type.strip() else None,
|
|
89
|
+
creator=actual_creator if actual_creator.strip() else None
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# If no parameters provided, search everything
|
|
93
|
+
if not search_query or search_query.strip() == "":
|
|
94
|
+
search_query = "*"
|
|
95
|
+
|
|
96
|
+
# Execute search using existing search client
|
|
97
|
+
if ctx:
|
|
98
|
+
await ctx.report_progress(0.5)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Use correct working pattern: search_utils.simple_search with existing search_client
|
|
102
|
+
search_results = search_utils.simple_search(search_client, search_query, max_items=actual_max_results)
|
|
103
|
+
|
|
104
|
+
if not search_results or not hasattr(search_results, 'list_'):
|
|
105
|
+
return safe_format_output(f"ERROR: Search failed - invalid response from Alfresco")
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Search failed: {e}")
|
|
109
|
+
return safe_format_output(f"ERROR: Search failed: {str(e)}")
|
|
110
|
+
|
|
111
|
+
# Process results using correct pattern
|
|
112
|
+
entries = []
|
|
113
|
+
if hasattr(search_results, 'list_') and search_results.list_ and hasattr(search_results.list_, 'entries'):
|
|
114
|
+
entries = search_results.list_.entries if search_results.list_ else []
|
|
115
|
+
|
|
116
|
+
if ctx:
|
|
117
|
+
await ctx.report_progress(1.0)
|
|
118
|
+
|
|
119
|
+
# Process final results
|
|
120
|
+
if entries:
|
|
121
|
+
logger.info(f"Found {len(entries)} search results")
|
|
122
|
+
result_text = f"Found {len(entries)} item(s) matching the metadata criteria:\n\n"
|
|
123
|
+
|
|
124
|
+
for i, entry in enumerate(entries, 1):
|
|
125
|
+
# Debug: Log the entry structure
|
|
126
|
+
logger.debug(f"Entry {i} type: {type(entry)}, content: {entry}")
|
|
127
|
+
|
|
128
|
+
# Handle different possible entry structures
|
|
129
|
+
node = None
|
|
130
|
+
if isinstance(entry, dict):
|
|
131
|
+
if 'entry' in entry:
|
|
132
|
+
node = entry['entry']
|
|
133
|
+
elif 'name' in entry: # Direct node structure
|
|
134
|
+
node = entry
|
|
135
|
+
else:
|
|
136
|
+
logger.warning(f"Unknown entry structure: {entry}")
|
|
137
|
+
continue
|
|
138
|
+
elif hasattr(entry, 'entry'): # ResultSetRowEntry object
|
|
139
|
+
node = entry.entry
|
|
140
|
+
else:
|
|
141
|
+
logger.warning(f"Entry is not a dict or ResultSetRowEntry: {type(entry)}")
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
if node:
|
|
145
|
+
# Handle both dict and ResultNode objects
|
|
146
|
+
if isinstance(node, dict):
|
|
147
|
+
name = str(node.get('name', 'Unknown'))
|
|
148
|
+
node_id = str(node.get('id', 'Unknown'))
|
|
149
|
+
node_type_actual = str(node.get('nodeType', 'Unknown'))
|
|
150
|
+
created_at = str(node.get('createdAt', 'Unknown'))
|
|
151
|
+
else:
|
|
152
|
+
# ResultNode object - access attributes directly
|
|
153
|
+
name = str(getattr(node, 'name', 'Unknown'))
|
|
154
|
+
node_id = str(getattr(node, 'id', 'Unknown'))
|
|
155
|
+
node_type_actual = str(getattr(node, 'node_type', 'Unknown'))
|
|
156
|
+
created_at = str(getattr(node, 'created_at', 'Unknown'))
|
|
157
|
+
|
|
158
|
+
# Clean JSON-friendly formatting (no markdown syntax)
|
|
159
|
+
# Apply safe formatting to individual fields to prevent emoji encoding issues
|
|
160
|
+
safe_name = safe_format_output(name)
|
|
161
|
+
safe_node_id = safe_format_output(node_id)
|
|
162
|
+
safe_node_type = safe_format_output(node_type_actual)
|
|
163
|
+
safe_created_at = safe_format_output(created_at)
|
|
164
|
+
|
|
165
|
+
result_text += f"{i}. {safe_name}\n"
|
|
166
|
+
result_text += f" - ID: {safe_node_id}\n"
|
|
167
|
+
result_text += f" - Type: {safe_node_type}\n"
|
|
168
|
+
result_text += f" - Created: {safe_created_at}\n\n"
|
|
169
|
+
|
|
170
|
+
return safe_format_output(result_text)
|
|
171
|
+
else:
|
|
172
|
+
# Simple "0" for zero results as requested
|
|
173
|
+
return "0"
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
# Preserve Unicode characters in error messages
|
|
177
|
+
error_msg = f"ERROR: Metadata search failed: {str(e)}"
|
|
178
|
+
if ctx:
|
|
179
|
+
await ctx.error(safe_format_output(error_msg))
|
|
180
|
+
return safe_format_output(error_msg)
|
|
181
|
+
|
|
182
|
+
if ctx:
|
|
183
|
+
await ctx.info(safe_format_output("Metadata search completed!"))
|