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.
Files changed (34) hide show
  1. alfresco_mcp_server/__init__.py +27 -0
  2. alfresco_mcp_server/config.py +104 -0
  3. alfresco_mcp_server/fastmcp_server.py +259 -0
  4. alfresco_mcp_server/prompts/__init__.py +7 -0
  5. alfresco_mcp_server/prompts/search_and_analyze.py +67 -0
  6. alfresco_mcp_server/resources/__init__.py +7 -0
  7. alfresco_mcp_server/resources/repository_resources.py +205 -0
  8. alfresco_mcp_server/tools/__init__.py +13 -0
  9. alfresco_mcp_server/tools/core/__init__.py +27 -0
  10. alfresco_mcp_server/tools/core/browse_repository.py +164 -0
  11. alfresco_mcp_server/tools/core/cancel_checkout.py +160 -0
  12. alfresco_mcp_server/tools/core/checkin_document.py +264 -0
  13. alfresco_mcp_server/tools/core/checkout_document.py +258 -0
  14. alfresco_mcp_server/tools/core/create_folder.py +105 -0
  15. alfresco_mcp_server/tools/core/delete_node.py +88 -0
  16. alfresco_mcp_server/tools/core/download_document.py +214 -0
  17. alfresco_mcp_server/tools/core/get_node_properties.py +195 -0
  18. alfresco_mcp_server/tools/core/update_node_properties.py +138 -0
  19. alfresco_mcp_server/tools/core/upload_document.py +244 -0
  20. alfresco_mcp_server/tools/search/__init__.py +15 -0
  21. alfresco_mcp_server/tools/search/advanced_search.py +204 -0
  22. alfresco_mcp_server/tools/search/cmis_search.py +180 -0
  23. alfresco_mcp_server/tools/search/search_by_metadata.py +183 -0
  24. alfresco_mcp_server/tools/search/search_content.py +186 -0
  25. alfresco_mcp_server/utils/__init__.py +34 -0
  26. alfresco_mcp_server/utils/connection.py +114 -0
  27. alfresco_mcp_server/utils/file_type_analysis.py +163 -0
  28. alfresco_mcp_server/utils/json_utils.py +135 -0
  29. python_alfresco_mcp_server-1.1.0.dist-info/METADATA +631 -0
  30. python_alfresco_mcp_server-1.1.0.dist-info/RECORD +34 -0
  31. python_alfresco_mcp_server-1.1.0.dist-info/WHEEL +5 -0
  32. python_alfresco_mcp_server-1.1.0.dist-info/entry_points.txt +2 -0
  33. python_alfresco_mcp_server-1.1.0.dist-info/licenses/LICENSE +201 -0
  34. python_alfresco_mcp_server-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,195 @@
1
+ """
2
+ Get node properties tool for Alfresco MCP Server.
3
+ Self-contained tool for retrieving document/folder metadata and properties.
4
+ """
5
+ import logging
6
+ from typing import Optional
7
+ from fastmcp import Context
8
+
9
+ from ...utils.connection import ensure_connection, get_core_client
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ async def get_node_properties_impl(node_id: str, ctx: Optional[Context] = None) -> str:
15
+ """Get metadata and properties of a document or folder.
16
+
17
+ Args:
18
+ node_id: Node ID to get properties for
19
+ ctx: MCP context for progress reporting
20
+
21
+ Returns:
22
+ Formatted node properties and metadata
23
+ """
24
+ if ctx:
25
+ await ctx.info(f"Getting properties for: {node_id}")
26
+ await ctx.report_progress(0.1)
27
+
28
+ if not node_id.strip():
29
+ return "ERROR: node_id is required"
30
+
31
+ try:
32
+ # Ensure connection and get core client
33
+ await ensure_connection()
34
+ core_client = await get_core_client()
35
+
36
+ # Clean the node ID (remove any URL encoding or extra characters)
37
+ clean_node_id = node_id.strip()
38
+ if clean_node_id.startswith('alfresco://'):
39
+ # Extract node ID from URI format
40
+ clean_node_id = clean_node_id.split('/')[-1]
41
+
42
+ logger.info(f"Getting properties for node: {clean_node_id}")
43
+
44
+ if ctx:
45
+ await ctx.report_progress(0.5)
46
+
47
+ # Get node metadata using core client
48
+ node_response = core_client.nodes.get(
49
+ node_id=clean_node_id,
50
+ include=["properties", "permissions", "path"]
51
+ )
52
+
53
+ if not hasattr(node_response, 'entry'):
54
+ return f"ERROR: Failed to get node information for: {clean_node_id}"
55
+
56
+ node_info = node_response.entry
57
+
58
+ # Extract basic properties - use correct Python attribute names
59
+ filename = getattr(node_info, 'name', 'Unknown')
60
+ node_type_raw = getattr(node_info, 'node_type', 'Unknown')
61
+ created_at = getattr(node_info, 'created_at', 'Unknown')
62
+ modified_at = getattr(node_info, 'modified_at', 'Unknown')
63
+
64
+ # Clean up node_type display - convert enum to string
65
+ node_type = 'Unknown'
66
+ if node_type_raw != 'Unknown':
67
+ if hasattr(node_type_raw, 'value'):
68
+ node_type = node_type_raw.value # For enum objects
69
+ else:
70
+ node_type = str(node_type_raw) # For string objects
71
+
72
+ # Extract creator and modifier information - use correct attribute names
73
+ creator = 'Unknown'
74
+ modifier = 'Unknown'
75
+ if hasattr(node_info, 'created_by_user') and node_info.created_by_user:
76
+ creator = getattr(node_info.created_by_user, 'display_name', 'Unknown')
77
+ if hasattr(node_info, 'modified_by_user') and node_info.modified_by_user:
78
+ modifier = getattr(node_info.modified_by_user, 'display_name', 'Unknown')
79
+
80
+ # Extract size information - use correct attribute names
81
+ size_str = 'Unknown'
82
+ if hasattr(node_info, 'content') and node_info.content:
83
+ size_bytes = getattr(node_info.content, 'size_in_bytes', 0)
84
+ if size_bytes > 0:
85
+ if size_bytes > 1024 * 1024:
86
+ size_str = f"{size_bytes / (1024 * 1024):.1f} MB"
87
+ elif size_bytes > 1024:
88
+ size_str = f"{size_bytes / 1024:.1f} KB"
89
+ else:
90
+ size_str = f"{size_bytes} bytes"
91
+
92
+ # Extract MIME type - use correct attribute names
93
+ mime_type = 'Unknown'
94
+ if hasattr(node_info, 'content') and node_info.content:
95
+ mime_type = getattr(node_info.content, 'mime_type', 'Unknown')
96
+
97
+ # Extract path information - use correct attribute names
98
+ path = 'Unknown'
99
+ if hasattr(node_info, 'path') and node_info.path:
100
+ path = getattr(node_info.path, 'name', 'Unknown')
101
+
102
+ # Extract custom properties - try multiple access methods
103
+ title = 'Unknown'
104
+ description = 'Unknown'
105
+ author = 'Unknown'
106
+ if hasattr(node_info, 'properties') and node_info.properties:
107
+ try:
108
+ # Try to_dict() method first
109
+ if hasattr(node_info.properties, 'to_dict'):
110
+ props_dict = node_info.properties.to_dict()
111
+ title = props_dict.get('cm:title', 'Unknown')
112
+ description = props_dict.get('cm:description', 'Unknown')
113
+ author = props_dict.get('cm:author', 'Unknown')
114
+ logger.info(f"Properties found via to_dict(): title={title}, description={description}, author={author}")
115
+ # Try direct attribute access
116
+ elif hasattr(node_info.properties, 'cm_title') or hasattr(node_info.properties, 'cm:title'):
117
+ title = getattr(node_info.properties, 'cm_title', getattr(node_info.properties, 'cm:title', 'Unknown'))
118
+ description = getattr(node_info.properties, 'cm_description', getattr(node_info.properties, 'cm:description', 'Unknown'))
119
+ author = getattr(node_info.properties, 'cm_author', getattr(node_info.properties, 'cm:author', 'Unknown'))
120
+ logger.info(f"Properties found via attributes: title={title}, description={description}, author={author}")
121
+ # Try dict-like access
122
+ elif hasattr(node_info.properties, '__getitem__'):
123
+ title = node_info.properties.get('cm:title', 'Unknown') if hasattr(node_info.properties, 'get') else node_info.properties['cm:title'] if 'cm:title' in node_info.properties else 'Unknown'
124
+ description = node_info.properties.get('cm:description', 'Unknown') if hasattr(node_info.properties, 'get') else node_info.properties['cm:description'] if 'cm:description' in node_info.properties else 'Unknown'
125
+ author = node_info.properties.get('cm:author', 'Unknown') if hasattr(node_info.properties, 'get') else node_info.properties['cm:author'] if 'cm:author' in node_info.properties else 'Unknown'
126
+ logger.info(f"Properties found via dict access: title={title}, description={description}, author={author}")
127
+ else:
128
+ logger.warning(f"Properties object type: {type(node_info.properties)}, available methods: {dir(node_info.properties)}")
129
+ except Exception as props_error:
130
+ logger.error(f"Error accessing properties: {props_error}")
131
+ else:
132
+ logger.warning("No properties found on node_info")
133
+
134
+ # Determine if it's a folder and if it's locked - use correct attribute names
135
+ is_folder = 'Yes' if node_type == 'cm:folder' else 'No'
136
+ is_locked = 'Unknown'
137
+ if hasattr(node_info, 'is_locked'):
138
+ is_locked = 'Yes' if node_info.is_locked else 'No'
139
+
140
+ # Version information - use correct attribute names
141
+ version = 'Unknown'
142
+ if hasattr(node_info, 'properties') and node_info.properties:
143
+ if hasattr(node_info.properties, 'to_dict'):
144
+ props_dict = node_info.properties.to_dict()
145
+ version = props_dict.get('cm:versionLabel', 'Unknown')
146
+
147
+ logger.info(f"Retrieved properties for: {filename}")
148
+
149
+ if ctx:
150
+ await ctx.report_progress(1.0)
151
+
152
+ # Clean JSON-friendly formatting (no markdown syntax)
153
+ # Only show properties that exist and have meaningful values
154
+ result = f"Node Properties for: {filename}\n\n"
155
+ result += f"Node ID: {clean_node_id}\n"
156
+ result += f"Name: {filename}\n"
157
+
158
+ # Only show properties that exist and aren't "Unknown"
159
+ if path and path != 'Unknown':
160
+ result += f"Path: {path}\n"
161
+ if node_type and node_type != 'Unknown':
162
+ result += f"Type: {node_type}\n"
163
+ if created_at and created_at != 'Unknown':
164
+ result += f"Created: {created_at}\n"
165
+ if modified_at and modified_at != 'Unknown':
166
+ result += f"Modified: {modified_at}\n"
167
+ if creator and creator != 'Unknown':
168
+ result += f"Creator: {creator}\n"
169
+ if modifier and modifier != 'Unknown':
170
+ result += f"Modifier: {modifier}\n"
171
+ if size_str and size_str != 'Unknown':
172
+ result += f"Size: {size_str}\n"
173
+ if mime_type and mime_type != 'Unknown':
174
+ result += f"MIME Type: {mime_type}\n"
175
+ if title and title != 'Unknown':
176
+ result += f"Title: {title}\n"
177
+ if description and description != 'Unknown':
178
+ result += f"Description: {description}\n"
179
+ if author and author != 'Unknown':
180
+ result += f"Author: {author}\n"
181
+ if is_folder and is_folder != 'Unknown':
182
+ result += f"Is Folder: {is_folder}\n"
183
+ if is_locked and is_locked != 'Unknown':
184
+ result += f"Is Locked: {is_locked}\n"
185
+ if version and version != 'Unknown':
186
+ result += f"Version: {version}\n"
187
+
188
+ return result
189
+
190
+ except Exception as e:
191
+ error_msg = f"ERROR: Failed to get properties: {str(e)}"
192
+ if ctx:
193
+ await ctx.error(error_msg)
194
+ logger.error(f"Get properties failed: {e}")
195
+ return error_msg
@@ -0,0 +1,138 @@
1
+ """
2
+ Update node properties tool for Alfresco MCP Server.
3
+ Self-contained tool for updating document/folder metadata and properties.
4
+ """
5
+ import logging
6
+ import os
7
+ from typing import Optional
8
+ from fastmcp import Context
9
+
10
+ from ...utils.connection import ensure_connection, get_core_client
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ async def update_node_properties_impl(
16
+ node_id: str,
17
+ name: str = "",
18
+ title: str = "",
19
+ description: str = "",
20
+ author: str = "",
21
+ ctx: Optional[Context] = None
22
+ ) -> str:
23
+ """Update metadata and properties of a document or folder.
24
+
25
+ Args:
26
+ node_id: Node ID to update (required)
27
+ name: New name for the node (optional)
28
+ title: Document title (cm:title) (optional)
29
+ description: Document description (cm:description) (optional)
30
+ author: Document author (cm:author) (optional)
31
+ ctx: MCP context for progress reporting
32
+
33
+ Returns:
34
+ Update confirmation with changes made
35
+ """
36
+ if ctx:
37
+ await ctx.info(f"Updating properties for: {node_id}")
38
+ await ctx.report_progress(0.1)
39
+
40
+ if not node_id.strip():
41
+ return "ERROR: node_id is required"
42
+
43
+ if not any([name, title, description, author]):
44
+ return "ERROR: At least one property (name, title, description, or author) must be provided"
45
+
46
+ try:
47
+ # Ensure connection and get core client
48
+ await ensure_connection()
49
+ core_client = await get_core_client()
50
+
51
+ # Clean the node ID (remove any URL encoding or extra characters)
52
+ clean_node_id = node_id.strip()
53
+ if clean_node_id.startswith('alfresco://'):
54
+ # Extract node ID from URI format
55
+ clean_node_id = clean_node_id.split('/')[-1]
56
+
57
+ logger.info(f"Updating properties for node: {clean_node_id}")
58
+
59
+ if ctx:
60
+ await ctx.report_progress(0.3)
61
+
62
+ # Get node information first to validate it exists
63
+ try:
64
+ node_response = core_client.nodes.get(node_id=clean_node_id)
65
+ if not hasattr(node_response, 'entry'):
66
+ return f"ERROR: Failed to get node information for: {clean_node_id}"
67
+
68
+ node_info = node_response.entry
69
+ current_name = getattr(node_info, 'name', f"document_{clean_node_id}")
70
+
71
+ except Exception as get_error:
72
+ return f"ERROR: Failed to validate node {clean_node_id}: {str(get_error)}"
73
+
74
+ if ctx:
75
+ await ctx.report_progress(0.5)
76
+
77
+ # Prepare updates for actual API call
78
+ properties_updates = {}
79
+ if title and title.strip():
80
+ properties_updates['cm:title'] = title.strip()
81
+ if description and description.strip():
82
+ properties_updates['cm:description'] = description.strip()
83
+ if author and author.strip():
84
+ properties_updates['cm:author'] = author.strip()
85
+
86
+ # Import the UpdateNodeRequest model
87
+ try:
88
+ from python_alfresco_api.clients.core.nodes.models import UpdateNodeRequest
89
+ except ImportError:
90
+ return "ERROR: Failed to import UpdateNodeRequest model"
91
+
92
+ # Prepare update request
93
+ update_request = UpdateNodeRequest()
94
+
95
+ if name and name.strip():
96
+ update_request.name = name.strip()
97
+ if properties_updates:
98
+ update_request.properties = properties_updates
99
+
100
+ if ctx:
101
+ await ctx.report_progress(0.7)
102
+
103
+ # Use the core client's update method
104
+ try:
105
+ updated_node = core_client.nodes.update(
106
+ node_id=clean_node_id,
107
+ request=update_request
108
+ )
109
+ logger.info("Node properties updated successfully")
110
+
111
+ except Exception as update_error:
112
+ logger.error(f"Update failed: {update_error}")
113
+ return f"ERROR: Update failed: {str(update_error)}"
114
+
115
+ if ctx:
116
+ await ctx.report_progress(1.0)
117
+
118
+ changes = []
119
+ if name:
120
+ changes.append(f"- Name: {name}")
121
+ if title:
122
+ changes.append(f"- Title: {title}")
123
+ if description:
124
+ changes.append(f"- Description: {description}")
125
+ if author:
126
+ changes.append(f"- Author: {author}")
127
+
128
+ updated_properties = "\n".join(changes)
129
+
130
+ # Clean JSON-friendly formatting (no markdown syntax)
131
+ return f"Node Updated Successfully\n\nNode ID: {clean_node_id}\nUpdated Properties:\n{updated_properties}\nUpdate completed successfully"
132
+
133
+ except Exception as e:
134
+ error_msg = f"ERROR: Update failed: {str(e)}"
135
+ if ctx:
136
+ await ctx.error(error_msg)
137
+ logger.error(f"Update failed: {e}")
138
+ return error_msg
@@ -0,0 +1,244 @@
1
+ """
2
+ Upload document tool for Alfresco MCP Server.
3
+ Self-contained tool for uploading documents to Alfresco repository.
4
+ """
5
+ import logging
6
+ import os
7
+ import base64
8
+ import tempfile
9
+ from typing import Optional
10
+ from fastmcp import Context
11
+
12
+ from ...utils.connection import ensure_connection, get_core_client
13
+ from ...utils.json_utils import safe_format_output
14
+ from ...utils.file_type_analysis import detect_file_extension_from_content
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ # Add this TEMPORARILY to your MCP server code:
20
+ def create_and_upload_file_share_style_temp(
21
+ core_client,
22
+ file_path,
23
+ parent_id="-my-",
24
+ filename=None,
25
+ description=None,
26
+ custom_title=None
27
+ ):
28
+ """
29
+ TEMPORARY: Share-style upload until next python-alfresco-api release.
30
+ Creates version 1.0 with full path as title (for real files) or custom title (for base64).
31
+ """
32
+ from pathlib import Path
33
+ from python_alfresco_api.utils import content_utils
34
+
35
+ file_path_obj = Path(file_path)
36
+ upload_filename = filename or file_path_obj.name
37
+
38
+ # Build properties with appropriate title
39
+ if custom_title:
40
+ # Use custom title for base64 uploads (just filename, not temp path)
41
+ properties = {"cm:title": custom_title}
42
+ else:
43
+ # Use full path for real file uploads (Share-style behavior)
44
+ properties = {"cm:title": str(file_path_obj)}
45
+
46
+ if description:
47
+ properties["cm:description"] = description
48
+
49
+ # Use existing content_utils.upload_file (already in current package)
50
+ return content_utils.upload_file(
51
+ core_client=core_client,
52
+ file_path=file_path_obj,
53
+ parent_id=parent_id,
54
+ filename=upload_filename,
55
+ description=description,
56
+ properties=properties,
57
+ auto_rename=True
58
+ )
59
+
60
+
61
+ async def upload_document_impl(
62
+ file_path: str = "",
63
+ base64_content: str = "",
64
+ parent_id: str = "-shared-",
65
+ description: str = "",
66
+ ctx: Optional[Context] = None
67
+ ) -> str:
68
+ """Upload a document to Alfresco using Share-style behavior.
69
+
70
+ Args:
71
+ file_path: Path to the file to upload (alternative to base64_content)
72
+ base64_content: Base64 encoded file content (alternative to file_path)
73
+ parent_id: Parent folder ID (default: shared folder)
74
+ description: Document description (optional)
75
+ ctx: MCP context for progress reporting
76
+
77
+ Note:
78
+ - Uploads as version 1.0 (matching Alfresco Share behavior)
79
+ - Uses full file path as title (matching Alfresco Share behavior)
80
+ - Original filename is preserved automatically
81
+ - For base64 uploads: content type detection and auto-naming
82
+ - Cross-platform support: Windows paths with quotes, macOS ~/path expansion, Linux XDG directories
83
+ - File extension detection works on all platforms (including macOS hidden extensions)
84
+ - Linux filesystem case-sensitivity and permission handling
85
+
86
+ Returns:
87
+ Upload confirmation with document details
88
+ """
89
+ if ctx:
90
+ if file_path:
91
+ await ctx.info(f">> Uploading document from '{file_path}' to {parent_id}")
92
+ else:
93
+ await ctx.info(f">> Uploading base64 content to {parent_id}")
94
+ await ctx.info("Validating file and parameters...")
95
+ await ctx.report_progress(0.1)
96
+
97
+ # Determine upload mode and validate
98
+ use_base64 = bool(base64_content.strip())
99
+ use_file_path = bool(file_path.strip())
100
+
101
+ if not use_base64 and not use_file_path:
102
+ return "ERROR: Must provide either file_path or base64_content"
103
+
104
+ if use_base64 and use_file_path:
105
+ return "ERROR: Cannot use both file_path and base64_content - choose one"
106
+
107
+ # Variables for upload
108
+ actual_file_path = None
109
+ temp_file_path = None
110
+ final_filename = None
111
+
112
+ try:
113
+ if use_file_path:
114
+ # Handle file path upload - cross-platform path handling
115
+ cleaned_file_path = file_path.strip().strip('"').strip("'")
116
+
117
+ # Handle macOS/Unix path expansion (~/Documents, etc.)
118
+ if cleaned_file_path.startswith('~'):
119
+ cleaned_file_path = os.path.expanduser(cleaned_file_path)
120
+
121
+ abs_file_path = os.path.abspath(cleaned_file_path)
122
+
123
+ if not os.path.exists(abs_file_path):
124
+ if os.path.exists(cleaned_file_path):
125
+ abs_file_path = cleaned_file_path
126
+ else:
127
+ return f"ERROR: File not found: {cleaned_file_path} (cleaned from: {file_path})"
128
+
129
+ if not os.path.isfile(abs_file_path):
130
+ return f"ERROR: Path is not a file: {abs_file_path}"
131
+
132
+ # Linux-specific: Check file permissions
133
+ if not os.access(abs_file_path, os.R_OK):
134
+ return f"ERROR: File not readable (permission denied): {abs_file_path}"
135
+
136
+ actual_file_path = abs_file_path
137
+ final_filename = os.path.basename(abs_file_path)
138
+
139
+ else:
140
+ # Handle base64 content upload - create temporary file
141
+ try:
142
+ file_content = base64.b64decode(base64_content)
143
+
144
+ # Detect content type and create appropriate filename
145
+ detected_extension = detect_file_extension_from_content(file_content)
146
+ final_filename = f"uploaded_document{detected_extension or ''}"
147
+
148
+ # Create temporary file with decoded content
149
+ temp_fd, temp_file_path = tempfile.mkstemp(suffix=f"_{final_filename}")
150
+ try:
151
+ with os.fdopen(temp_fd, 'wb') as temp_file:
152
+ temp_file.write(file_content)
153
+ actual_file_path = temp_file_path
154
+ except Exception:
155
+ os.close(temp_fd) # Close if writing failed
156
+ raise
157
+
158
+ except Exception as decode_error:
159
+ return f"ERROR: Invalid base64 content or file creation failed: {str(decode_error)}"
160
+
161
+ if not actual_file_path:
162
+ return "ERROR: No valid file path available for upload"
163
+
164
+ except Exception as validation_error:
165
+ return f"ERROR: Validation failed: {str(validation_error)}"
166
+
167
+ try:
168
+ # Ensure connection and get core client
169
+ await ensure_connection()
170
+ core_client = await get_core_client()
171
+
172
+ if not core_client.is_initialized:
173
+ return safe_format_output("❌ Error: Alfresco server unavailable")
174
+
175
+ if ctx:
176
+ await ctx.info("Creating and uploading document using Share-style approach...")
177
+ await ctx.report_progress(0.5)
178
+
179
+ logger.debug(f"Uploading '{final_filename}' to parent {parent_id} using Share-style function")
180
+
181
+ # Determine title based on upload type
182
+ custom_title = None
183
+ if use_base64:
184
+ # For base64 uploads, use just the filename as title (not temp file path)
185
+ custom_title = final_filename
186
+ # For file path uploads, let Share-style function use full path as title
187
+
188
+ # Use Share-style upload function
189
+ result = create_and_upload_file_share_style_temp(
190
+ core_client=core_client,
191
+ file_path=actual_file_path,
192
+ parent_id=parent_id,
193
+ filename=final_filename,
194
+ description=description or None,
195
+ custom_title=custom_title
196
+ )
197
+
198
+ # Extract essential info
199
+ if hasattr(result, 'entry') and result.entry:
200
+ node_id = getattr(result.entry, 'id', 'Unknown')
201
+ node_name = getattr(result.entry, 'name', final_filename)
202
+ logger.info(f"Upload completed: {node_name} -> {node_id}")
203
+ else:
204
+ logger.info(f"Upload completed successfully")
205
+
206
+ if ctx:
207
+ await ctx.info("Upload completed successfully!")
208
+ await ctx.report_progress(1.0)
209
+
210
+ # Format result for MCP with clean JSON-friendly output
211
+ title_info = custom_title if custom_title else "Full file path"
212
+ upload_type = "Base64 content" if use_base64 else "File path"
213
+
214
+ success_result = f"""SUCCESS: Document Uploaded Successfully!
215
+
216
+ Name: {final_filename}
217
+ Parent: {parent_id}
218
+ Title: {title_info}
219
+ Description: {description or 'N/A'}
220
+ Upload Type: {upload_type}
221
+
222
+ Details: {result}
223
+
224
+ ✨ Share-Style Upload: Version 1.0, proper title handling for upload type
225
+ ✨ File path uploads: Full path as title (matching Alfresco Share)
226
+ ✨ Base64 uploads: Clean filename as title (no temp file paths)"""
227
+
228
+ return success_result
229
+
230
+ except Exception as e:
231
+ error_msg = f"ERROR: Document upload failed: {str(e)}"
232
+ if ctx:
233
+ await ctx.error(error_msg)
234
+ logger.error(f"Document upload failed: {e}")
235
+ return error_msg
236
+
237
+ finally:
238
+ # Clean up temporary file if created
239
+ if temp_file_path and os.path.exists(temp_file_path):
240
+ try:
241
+ os.unlink(temp_file_path)
242
+ logger.debug(f"Cleaned up temporary file: {temp_file_path}")
243
+ except Exception as cleanup_error:
244
+ logger.warning(f"Failed to clean up temporary file {temp_file_path}: {cleanup_error}")
@@ -0,0 +1,15 @@
1
+ # Search tools module
2
+
3
+ from . import (
4
+ advanced_search,
5
+ cmis_search,
6
+ search_by_metadata,
7
+ search_content,
8
+ )
9
+
10
+ __all__ = [
11
+ "advanced_search",
12
+ "cmis_search",
13
+ "search_by_metadata",
14
+ "search_content",
15
+ ]