universal-mcp-applications 0.1.25__py3-none-any.whl → 0.1.32__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.
@@ -1,17 +1,19 @@
1
- # SharepointApp MCP Server
1
+ # SharePoint Application
2
2
 
3
- An MCP Server for the SharepointApp API.
3
+ This application provides tools for interacting with the Microsoft SharePoint API via Microsoft Graph. It allows you to manage files, folders, and retrieve information about your SharePoint drive.
4
4
 
5
- ## 🛠️ Tool List
5
+ ## Available Tools
6
6
 
7
- This is automatically generated from OpenAPI schema for the SharepointApp API.
8
-
9
-
10
- | Tool | Description |
11
- |------|-------------|
12
- | `list_folders` | Retrieves a list of immediate subfolder names within a specified SharePoint directory. If no path is provided, it defaults to the root drive. This function is distinct from `list_files`, as it exclusively lists directories, not files. |
13
- | `create_folder_and_list` | Creates a new folder with a given name inside a specified parent directory (or the root). It then returns an updated list of all folder names within that same directory, effectively confirming that the creation operation was successful. |
14
- | `list_files` | Retrieves metadata for all files in a specified folder. For each file, it returns key details like name, URL, size, and timestamps. This function exclusively lists file properties, distinguishing it from `list_folders` (which lists directories) and `get_document_content` (which retrieves file content). |
15
- | `upload_text_file` | Uploads string content to create a new file in a specified SharePoint folder. To confirm the operation, it returns an updated list of all files and their metadata from that directory, including the newly created file. |
16
- | `get_document_content` | Retrieves a file's content from a specified SharePoint path. It returns a dictionary containing the file's name and size, decoding text files as a string and Base64-encoding binary files. Unlike `list_files`, which only fetches metadata, this function provides the actual file content. |
17
- | `delete_document` | Permanently deletes a specified file from a SharePoint drive using its full path. This is the sole destructive file operation, contrasting with functions that read or create files. It returns `True` on successful deletion and raises an exception on failure, such as if the file is not found. |
7
+ - `get_my_profile`: Fetches the profile for the currently authenticated user.
8
+ - `get_drive_info`: Fetches high-level information about the user's SharePoint drive.
9
+ - `search_files`: Searches for files and folders in the user's SharePoint.
10
+ - `get_item_metadata`: Fetches metadata for a specific file or folder.
11
+ - `create_folder`: Creates a new folder.
12
+ - `delete_item`: Deletes a file or folder.
13
+ - `download_file`: Retrieves a download URL for a file.
14
+ - `upload_file`: Uploads a local file.
15
+ - `list_folders`: Lists all folders in a specified directory.
16
+ - `list_files`: Lists all files in a specified directory.
17
+ - `create_folder_and_list`: Creates a folder and then lists the contents of the parent directory.
18
+ - `upload_text_file`: Uploads content from a string to a new text file.
19
+ - `get_document_content`: Retrieves the content of a file.
@@ -1,225 +1,338 @@
1
1
  import base64
2
- import io
3
- from datetime import datetime
4
- from io import BytesIO
2
+ import os
5
3
  from typing import Any
6
4
 
7
5
  from loguru import logger
8
- from office365.graph_client import GraphClient
9
- from universal_mcp.applications.application import BaseApplication
6
+ from universal_mcp.applications.application import APIApplication
10
7
  from universal_mcp.integrations import Integration
11
8
 
12
9
 
13
- def _to_iso_optional(dt_obj: datetime | None) -> str | None:
14
- """Converts a datetime object to ISO format string, or returns None if the object is None."""
15
- if dt_obj is not None:
16
- return dt_obj.isoformat()
17
- return None
18
-
19
-
20
- class SharepointApp(BaseApplication):
10
+ class SharepointApp(APIApplication):
21
11
  """
22
- Base class for Universal MCP Applications.
12
+ Application for interacting with Microsoft SharePoint API (via Microsoft Graph).
13
+ Provides tools to manage files, folders, and access Drive information.
23
14
  """
24
15
 
25
- def __init__(self, integration: Integration = None, client=None, **kwargs) -> None:
26
- """Initializes the SharepointApp.
16
+ def __init__(self, integration: Integration | None = None, **kwargs) -> None:
17
+ super().__init__(name="sharepoint", integration=integration, **kwargs)
18
+ self.base_url = "https://graph.microsoft.com/v1.0"
19
+
20
+ def get_my_profile(self) -> dict[str, Any]:
21
+ """
22
+ Fetches the profile for the currently authenticated user, specifically retrieving their ID and user principal name. This function confirms user identity, distinguishing it from `get_drive_info`, which returns details about the SharePoint storage space (e.g., quota) rather than the user's personal profile.
23
+
24
+ Returns:
25
+ dict[str, Any]: A dictionary containing the user's id and userPrincipalName.
26
+
27
+ Raises:
28
+ HTTPStatusError: If the API request fails.
29
+
30
+ Tags:
31
+ profile, user, account
32
+ """
33
+ url = f"{self.base_url}/me"
34
+ query_params = {"$select": "id,userPrincipalName"}
35
+ response = self._get(url, params=query_params)
36
+ return self._handle_response(response)
37
+
38
+ def get_drive_info(self) -> dict[str, Any]:
39
+ """
40
+ Fetches high-level information about the user's entire SharePoint. It returns drive-wide details like the owner and storage quota, differing from `get_item_metadata` which describes a specific item, and `get_my_profile` which retrieves general user account information.
41
+
42
+ Returns:
43
+ A dictionary containing drive information.
44
+
45
+ Tags:
46
+ drive, storage, quota, info
47
+ """
48
+ url = f"{self.base_url}/me/drive"
49
+ response = self._get(url)
50
+ return self._handle_response(response)
51
+
52
+ def _list_drive_items(self, item_id: str = "root") -> dict[str, Any]:
53
+ """
54
+ Lists the files and folders in the current user's SharePoint.
55
+
56
+ Args:
57
+ item_id (str, optional): The ID of the folder to list. Defaults to 'root'.
58
+
59
+ Returns:
60
+ A dictionary containing the list of files and folders.
61
+ """
62
+ url = f"{self.base_url}/me/drive/items/{item_id}/children"
63
+ response = self._get(url)
64
+ return self._handle_response(response)
65
+
66
+ def search_files(self, query: str) -> dict[str, Any]:
67
+ """
68
+ Searches the user's entire SharePoint for files and folders matching a specified text query. This function performs a comprehensive search from the drive's root, distinguishing it from `list_files` or `list_folders` which only browse the contents of a single directory.
69
+
27
70
  Args:
28
- client (GraphClient | None, optional): An existing GraphClient instance. If None, a new client will be created on first use.
71
+ query (str): The search query.
72
+
73
+ Returns:
74
+ A dictionary containing the search results.
75
+
76
+ Tags:
77
+ search, find, query, files, important
29
78
  """
30
- super().__init__(name="sharepoint", integration=integration, **kwargs)
31
- self._client = client
32
- self.integration = integration
33
- self._site_url = None
79
+ if not query:
80
+ raise ValueError("Search query cannot be empty.")
81
+
82
+ url = f"{self.base_url}/me/drive/root/search(q='{query}')"
83
+ response = self._get(url)
84
+ return self._handle_response(response)
34
85
 
35
- @property
36
- def client(self):
86
+ def get_item_metadata(self, item_id: str) -> dict[str, Any]:
37
87
  """
38
- A lazy-loaded property that gets or creates an authenticated GraphClient instance. On first access, it uses integration credentials to initialize the client, fetches initial user and site data, and caches the instance for subsequent use, ensuring efficient connection management for all SharePoint operations.
88
+ Fetches detailed metadata for a specific file or folder using its unique ID. It returns properties like name, size, and type. Unlike `get_document_content`, it doesn't retrieve the file's actual content, focusing solely on the item's attributes for quick inspection without a full download.
89
+
90
+ Args:
91
+ item_id (str): The ID of the file or folder.
39
92
 
40
93
  Returns:
41
- GraphClient: The authenticated GraphClient instance.
42
- """
43
- if not self.integration:
44
- raise ValueError("Integration is required")
45
- credentials = self.integration.get_credentials()
46
- if not credentials:
47
- raise ValueError("No credentials found")
48
-
49
- if not credentials.get("access_token"):
50
- raise ValueError("No access token found")
51
-
52
- def _acquire_token():
53
- """
54
- Formats stored credentials for the `GraphClient` authentication callback. It packages existing access and refresh tokens from the integration into the specific dictionary structure required by the client library for authentication, including a hardcoded 'Bearer' token type.
55
- """
56
- access_token = credentials.get("access_token")
57
- refresh_token = credentials.get("refresh_token")
58
- return {
59
- "access_token": access_token,
60
- "refresh_token": refresh_token,
61
- "token_type": "Bearer",
62
- }
63
-
64
- if self._client is None:
65
- self._client = GraphClient(token_callback=_acquire_token)
66
- # Get me
67
- me = self._client.me.get().execute_query()
68
- logger.debug(me.properties)
69
- # Get sites
70
- sites = self._client.sites.root.get().execute_query()
71
- self._site_url = sites.properties.get("id")
72
- return self._client
73
-
74
- def list_folders(self, folder_path: str | None = None) -> list[dict[str, Any]]:
75
- """
76
- Retrieves a list of immediate subfolder names within a specified SharePoint directory. If no path is provided, it defaults to the root drive. This function is distinct from `list_files`, as it exclusively lists directories, not files.
94
+ A dictionary containing the item's metadata.
95
+
96
+ Tags:
97
+ metadata, info, file, folder
98
+ """
99
+ if not item_id:
100
+ raise ValueError("Missing required parameter 'item_id'.")
101
+
102
+ url = f"{self.base_url}/me/drive/items/{item_id}"
103
+ response = self._get(url)
104
+ return self._handle_response(response)
105
+
106
+ def create_folder(self, name: str, parent_id: str = "root") -> dict[str, Any]:
107
+ """
108
+ Creates a new folder with a specified name within a parent directory, which defaults to the root. Returns metadata for the new folder. Unlike `create_folder_and_list`, this function only creates the folder and returns its specific metadata, not the parent directory's contents.
77
109
 
78
110
  Args:
79
- folder_path (Optional[str], optional): The path to the parent folder. If None, lists folders in the root.
111
+ name (str): The name of the new folder.
112
+ parent_id (str, optional): The ID of the parent folder. Defaults to 'root'.
80
113
 
81
114
  Returns:
82
- List[Dict[str, Any]]: A list of folder names in the specified directory.
115
+ A dictionary containing the new folder's metadata.
83
116
 
84
117
  Tags:
85
- important
118
+ create, folder, directory, important
86
119
  """
87
- if folder_path:
88
- folder = self.client.me.drive.root.get_by_path(folder_path)
89
- folders = folder.get_folders(False).execute_query()
90
- else:
91
- folders = self.client.me.drive.root.get_folders(False).execute_query()
120
+ if not name:
121
+ raise ValueError("Folder name cannot be empty.")
92
122
 
93
- return [folder.properties.get("name") for folder in folders]
123
+ url = f"{self.base_url}/me/drive/items/{parent_id}/children"
124
+ data = {"name": name, "folder": {}, "@microsoft.graph.conflictBehavior": "rename"}
125
+ response = self._post(url, data=data)
126
+ return self._handle_response(response)
94
127
 
95
- def create_folder_and_list(
96
- self, folder_name: str, folder_path: str | None = None
97
- ) -> dict[str, Any]:
128
+ def delete_item(self, item_id: str) -> dict[str, Any]:
98
129
  """
99
- Creates a new folder with a given name inside a specified parent directory (or the root). It then returns an updated list of all folder names within that same directory, effectively confirming that the creation operation was successful.
130
+ Permanently deletes a specified file or folder from SharePoint using its unique item ID. This versatile function can remove any type of drive item, distinguished from functions that only list or create specific types. A successful deletion returns an empty response, confirming the item's removal.
100
131
 
101
132
  Args:
102
- folder_name (str): The name of the folder to create.
103
- folder_path (str | None, optional): The path to the parent folder. If None, creates in the root.
133
+ item_id (str): The ID of the item to delete.
104
134
 
105
135
  Returns:
106
- Dict[str, Any]: The updated list of folders in the target directory.
136
+ An empty dictionary if successful.
107
137
 
108
138
  Tags:
109
- important
139
+ delete, remove, file, folder, important
110
140
  """
111
- if folder_path:
112
- folder = self.client.me.drive.root.get_by_path(folder_path)
113
- else:
114
- folder = self.client.me.drive.root
115
- folder.create_folder(folder_name).execute_query()
116
- return self.list_folders(folder_path)
141
+ if not item_id:
142
+ raise ValueError("Missing required parameter 'item_id'.")
143
+
144
+ url = f"{self.base_url}/me/drive/items/{item_id}"
145
+ response = self._delete(url)
146
+ return self._handle_response(response)
117
147
 
118
- def list_files(self, folder_path: str) -> list[dict[str, Any]]:
148
+ def download_file(self, item_id: str) -> dict[str, Any]:
119
149
  """
120
- Retrieves metadata for all files in a specified folder. For each file, it returns key details like name, URL, size, and timestamps. This function exclusively lists file properties, distinguishing it from `list_folders` (which lists directories) and `get_document_content` (which retrieves file content).
150
+ Retrieves a temporary, pre-authenticated download URL for a specific file using its item ID. This function provides a link for subsequent download, differing from `get_document_content` which directly fetches the file's raw content. The URL is returned within a dictionary.
121
151
 
122
152
  Args:
123
- folder_path (str): The path to the folder whose documents are to be listed.
153
+ item_id (str): The ID of the file to download.
124
154
 
125
155
  Returns:
126
- List[Dict[str, Any]]: A list of dictionaries containing document metadata.
156
+ A dictionary containing the download URL for the file under the key '@microsoft.graph.downloadUrl'.
127
157
 
128
158
  Tags:
129
- important
159
+ download, file, get, important
160
+ """
161
+ if not item_id:
162
+ raise ValueError("Missing required parameter 'item_id'.")
163
+
164
+ url = f"{self.base_url}/me/drive/items/{item_id}"
165
+ response = self._get(url)
166
+ metadata = self._handle_response(response)
167
+ download_url = metadata.get("@microsoft.graph.downloadUrl")
168
+ if not download_url:
169
+ raise ValueError("Could not retrieve download URL for the item.")
170
+ return {"download_url": download_url}
171
+
172
+ def upload_file(self, file_path: str, parent_id: str = "root", file_name: str | None = None) -> dict[str, Any]:
130
173
  """
131
- folder = self.client.me.drive.root.get_by_path(folder_path)
132
- files = folder.get_files(False).execute_query()
174
+ Uploads a local binary file (under 4MB) from a given path to a specified SharePoint folder. Unlike `upload_text_file`, which uploads string content, this function reads from the filesystem. The destination filename can be customized, and it returns the new file's metadata upon completion.
133
175
 
134
- return [
135
- {
136
- "name": f.name,
137
- "url": f.properties.get("ServerRelativeUrl"),
138
- "size": f.properties.get("Length"),
139
- "created": _to_iso_optional(f.properties.get("TimeCreated")),
140
- "modified": _to_iso_optional(f.properties.get("TimeLastModified")),
141
- }
142
- for f in files
143
- ]
144
-
145
- def upload_text_file(
146
- self, file_path: str, file_name: str, content: str
147
- ) -> dict[str, Any]:
148
- """
149
- Uploads string content to create a new file in a specified SharePoint folder. To confirm the operation, it returns an updated list of all files and their metadata from that directory, including the newly created file.
176
+ Args:
177
+ file_path (str): The local path to the file to upload.
178
+ parent_id (str, optional): The ID of the folder to upload the file to. Defaults to 'root'.
179
+ file_name (str, optional): The name to give the uploaded file. If not provided, the local filename is used.
180
+
181
+ Returns:
182
+ A dictionary containing the uploaded file's metadata.
183
+
184
+ Tags:
185
+ upload, file, put, important
186
+ """
187
+ if not os.path.exists(file_path):
188
+ raise FileNotFoundError(f"The file was not found at path: {file_path}")
189
+
190
+ if not file_name:
191
+ file_name = os.path.basename(file_path)
192
+
193
+ url = f"{self.base_url}/me/drive/items/{parent_id}:/{file_name}:/content"
194
+ with open(file_path, "rb") as f:
195
+ data = f.read()
196
+ response = self._put(url, data=data, content_type="application/octet-stream")
197
+ return self._handle_response(response)
198
+
199
+ def list_folders(self, item_id: str = "root") -> dict[str, Any]:
200
+ """
201
+ Retrieves a list of only the folders within a specified parent directory in SharePoint. Unlike `_list_drive_items` which returns all items, this function filters the results to exclude files. Defaults to the root directory if no parent `item_id` is provided.
150
202
 
151
203
  Args:
152
- file_path (str): The path to the folder where the document will be created.
153
- file_name (str): The name of the document to create.
154
- content (str): The content to write into the document.
204
+ item_id (str, optional): The ID of the folder to list from. Defaults to 'root'.
155
205
 
156
206
  Returns:
157
- Dict[str, Any]: The updated list of documents in the folder.
207
+ A dictionary containing the list of folders.
158
208
 
159
- Tags: important
209
+ Tags:
210
+ list, folders, directories, important
160
211
  """
161
- file = self.client.me.drive.root.get_by_path(file_path)
162
- file_io = io.StringIO(content)
163
- file_io.name = file_name
164
- file.upload_file(file_io).execute_query()
165
- return self.list_documents(file_path)
212
+ all_items = self._list_drive_items(item_id=item_id)
213
+ folders = [item for item in all_items.get("value", []) if "folder" in item]
214
+ return {"value": folders}
166
215
 
167
- def get_document_content(self, file_path: str) -> dict[str, Any]:
216
+ def list_files(self, item_id: str = "root") -> dict[str, Any]:
168
217
  """
169
- Retrieves a file's content from a specified SharePoint path. It returns a dictionary containing the file's name and size, decoding text files as a string and Base64-encoding binary files. Unlike `list_files`, which only fetches metadata, this function provides the actual file content.
218
+ Retrieves a list of files within a specified SharePoint folder, defaulting to the root. Unlike `_list_drive_items` which fetches all items, this function filters the results to exclusively return items identified as files, excluding any subdirectories.
170
219
 
171
220
  Args:
172
- file_path (str): The path to the document.
221
+ item_id (str, optional): The ID of the folder to list files from. Defaults to 'root'.
173
222
 
174
223
  Returns:
175
- Dict[str, Any]: A dictionary containing the document's name, content type, content (as text or base64), and size.
176
-
177
- Tags: important
178
- """
179
- file = self.client.me.drive.root.get_by_path(file_path).get().execute_query()
180
- content_stream = BytesIO()
181
- file.download(content_stream).execute_query()
182
- content_stream.seek(0)
183
- content = content_stream.read()
184
-
185
- is_text_file = file_path.lower().endswith(
186
- (".txt", ".csv", ".json", ".xml", ".html", ".md", ".js", ".css", ".py")
187
- )
188
- content_dict = (
189
- {"content": content.decode("utf-8")}
190
- if is_text_file
191
- else {"content_base64": base64.b64encode(content).decode("ascii")}
192
- )
193
- return {
194
- "name": file_path.split("/")[-1],
195
- "content_type": "text" if is_text_file else "binary",
196
- **content_dict,
197
- "size": len(content),
198
- }
224
+ A dictionary containing the list of files.
225
+
226
+ Tags:
227
+ list, files, documents, important
228
+ """
229
+ all_items = self._list_drive_items(item_id=item_id)
230
+ files = [item for item in all_items.get("value", []) if "file" in item]
231
+ return {"value": files}
232
+
233
+ def create_folder_and_list(self, name: str, parent_id: str = "root") -> dict[str, Any]:
234
+ """
235
+ Performs a composite action: creates a new folder, then lists all items (files and folders) within that parent directory. This confirms creation by returning the parent's updated contents, distinct from `create_folder` which only returns the new folder's metadata.
236
+
237
+ Args:
238
+ name (str): The name of the new folder.
239
+ parent_id (str, optional): The ID of the parent folder. Defaults to 'root'.
240
+
241
+ Returns:
242
+ A dictionary containing the list of items in the parent folder after creation.
243
+
244
+ Tags:
245
+ create, folder, list, important
246
+ """
247
+ self.create_folder(name=name, parent_id=parent_id)
248
+ return self._list_drive_items(item_id=parent_id)
249
+
250
+ def upload_text_file(self, content: str, parent_id: str = "root", file_name: str = "new_file.txt") -> dict[str, Any]:
251
+ """
252
+ Creates and uploads a new file to SharePoint directly from a string of text content. Unlike `upload_file`, which requires a local file path, this function is specifically for creating a text file from in-memory string data, with a customizable name and destination folder.
253
+
254
+ Args:
255
+ content (str): The text content to upload.
256
+ parent_id (str, optional): The ID of the folder to upload the file to. Defaults to 'root'.
257
+ file_name (str, optional): The name to give the uploaded file. Defaults to 'new_file.txt'.
258
+
259
+ Returns:
260
+ A dictionary containing the uploaded file's metadata.
261
+
262
+ Tags:
263
+ upload, text, file, create, important
264
+ """
265
+ if not file_name:
266
+ raise ValueError("File name cannot be empty.")
267
+
268
+ url = f"{self.base_url}/me/drive/items/{parent_id}:/{file_name}:/content"
269
+ data = content.encode("utf-8")
270
+ response = self._put(url, data=data, content_type="text/plain")
271
+ return self._handle_response(response)
199
272
 
200
- def delete_document(self, file_path: str):
273
+ def get_document_content(self, item_id: str) -> dict[str, Any]:
201
274
  """
202
- Permanently deletes a specified file from a SharePoint drive using its full path. This is the sole destructive file operation, contrasting with functions that read or create files. It returns `True` on successful deletion and raises an exception on failure, such as if the file is not found.
275
+ Retrieves the content of a specific file by its item ID and returns it directly as base64-encoded data. This function is distinct from `download_file`, which only provides a temporary URL for the content, and from `get_item_metadata`, which returns file attributes without the content itself. The function fetches the content by following the file's pre-authenticated download URL.
203
276
 
204
277
  Args:
205
- file_path (str): The path to the file to delete.
278
+ item_id (str): The ID of the file.
206
279
 
207
280
  Returns:
208
- bool: True if the file was deleted successfully.
281
+ dict[str, Any]: A dictionary containing the file details:
282
+ - 'type' (str): The general type of the file (e.g., "image", "audio", "video", "file").
283
+ - 'data' (str): The base64 encoded content of the file.
284
+ - 'mime_type' (str): The MIME type of the file.
285
+ - 'file_name' (str): The name of the file.
209
286
 
210
287
  Tags:
211
- important
288
+ get, content, read, file, important
212
289
  """
213
- file = self.client.me.drive.root.get_by_path(file_path)
214
- file.delete_object().execute_query()
215
- return True
290
+ if not item_id:
291
+ raise ValueError("Missing required parameter 'item_id'.")
292
+
293
+ metadata = self.get_item_metadata(item_id=item_id)
294
+ file_metadata = metadata.get("file")
295
+ if not file_metadata:
296
+ raise ValueError(f"Item with ID '{item_id}' is not a file.")
297
+
298
+ file_mime_type = file_metadata.get("mimeType", "application/octet-stream")
299
+ file_name = metadata.get("name")
300
+
301
+ download_url = metadata.get("@microsoft.graph.downloadUrl")
302
+ if not download_url:
303
+ logger.error(f"Could not find @microsoft.graph.downloadUrl in metadata for item {item_id}")
304
+ raise ValueError("Could not retrieve download URL for the item.")
305
+
306
+ response = self._get(download_url)
307
+
308
+ response.raise_for_status()
309
+
310
+ content = response.content
311
+
312
+ attachment_type = file_mime_type.split("/")[0] if "/" in file_mime_type else "file"
313
+ if attachment_type not in ["image", "audio", "video", "text"]:
314
+ attachment_type = "file"
315
+
316
+ return {
317
+ "type": attachment_type,
318
+ "data": content,
319
+ "mime_type": file_mime_type,
320
+ "file_name": file_name,
321
+ }
216
322
 
217
323
  def list_tools(self):
218
324
  return [
325
+ self.get_drive_info,
326
+ self.search_files,
327
+ self.get_item_metadata,
328
+ self.create_folder,
329
+ self.delete_item,
330
+ self.download_file,
331
+ self.upload_file,
332
+ self.get_my_profile,
219
333
  self.list_folders,
220
- self.create_folder_and_list,
221
334
  self.list_files,
335
+ self.create_folder_and_list,
222
336
  self.upload_text_file,
223
337
  self.get_document_content,
224
- self.delete_document,
225
- ]
338
+ ]
@@ -9,6 +9,37 @@ class SlackApp(APIApplication):
9
9
  super().__init__(name="slack", integration=integration, **kwargs)
10
10
  self.base_url = "https://slack.com/api"
11
11
 
12
+ def _get_headers(self) -> dict[str, str]:
13
+ """
14
+ Get headers for Slack API requests.
15
+ Prioritizes user-scoped access token from raw.authed_user.access_token
16
+ over the bot token at the root level.
17
+ """
18
+ if not self.integration:
19
+ raise ValueError("Integration not configured for SlackApp")
20
+
21
+ credentials = self.integration.get_credentials()
22
+ if not credentials:
23
+ raise ValueError("No credentials found for Slack integration")
24
+
25
+ access_token = None
26
+ raw = credentials.get('raw', {})
27
+ if isinstance(raw, dict) and 'authed_user' in raw:
28
+ authed_user = raw.get('authed_user', {})
29
+ if isinstance(authed_user, dict):
30
+ access_token = authed_user.get('access_token')
31
+
32
+ if not access_token:
33
+ access_token = credentials.get('access_token')
34
+
35
+ if not access_token:
36
+ raise ValueError("Access token not found in Slack credentials")
37
+
38
+ return {
39
+ "Authorization": f"Bearer {access_token}",
40
+ "Content-Type": "application/json",
41
+ }
42
+
12
43
  def chat_delete(
13
44
  self,
14
45
  as_user: bool | None = None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: universal-mcp-applications
3
- Version: 0.1.25
3
+ Version: 0.1.32
4
4
  Summary: A Universal MCP Application: universal_mcp_applications
5
5
  Project-URL: Homepage, https://github.com/universal-mcp/applications
6
6
  Project-URL: Repository, https://github.com/universal-mcp/applications
@@ -35,7 +35,7 @@ Requires-Dist: replicate>=1.0.6
35
35
  Requires-Dist: requests>=2.31.0
36
36
  Requires-Dist: resend>=2.10.0
37
37
  Requires-Dist: twilio>=9.8.0
38
- Requires-Dist: universal-mcp>=0.1.24rc21
38
+ Requires-Dist: universal-mcp>=0.1.24rc26
39
39
  Requires-Dist: yfinance>=0.2.66
40
40
  Requires-Dist: youtube-transcript-api==1.2.2
41
41
  Provides-Extra: dev