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,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}")
|