mcp-sharepoint-us 2.0.10__py3-none-any.whl → 2.0.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-sharepoint-us might be problematic. Click here for more details.
- mcp_sharepoint/__init__.py +110 -17
- mcp_sharepoint/auth.py +3 -2
- mcp_sharepoint/graph_api.py +289 -0
- {mcp_sharepoint_us-2.0.10.dist-info → mcp_sharepoint_us-2.0.12.dist-info}/METADATA +2 -1
- mcp_sharepoint_us-2.0.12.dist-info/RECORD +10 -0
- mcp_sharepoint_us-2.0.10.dist-info/RECORD +0 -9
- {mcp_sharepoint_us-2.0.10.dist-info → mcp_sharepoint_us-2.0.12.dist-info}/WHEEL +0 -0
- {mcp_sharepoint_us-2.0.10.dist-info → mcp_sharepoint_us-2.0.12.dist-info}/entry_points.txt +0 -0
- {mcp_sharepoint_us-2.0.10.dist-info → mcp_sharepoint_us-2.0.12.dist-info}/licenses/LICENSE +0 -0
- {mcp_sharepoint_us-2.0.10.dist-info → mcp_sharepoint_us-2.0.12.dist-info}/top_level.txt +0 -0
mcp_sharepoint/__init__.py
CHANGED
|
@@ -19,6 +19,7 @@ from office365.sharepoint.folders.folder import Folder
|
|
|
19
19
|
from office365.sharepoint.client_context import ClientContext
|
|
20
20
|
|
|
21
21
|
from .auth import create_sharepoint_context
|
|
22
|
+
from .graph_api import GraphAPIClient
|
|
22
23
|
|
|
23
24
|
# Setup logging
|
|
24
25
|
logging.basicConfig(level=logging.INFO)
|
|
@@ -27,19 +28,70 @@ logger = logging.getLogger(__name__)
|
|
|
27
28
|
# Initialize MCP server
|
|
28
29
|
app = Server("mcp-sharepoint")
|
|
29
30
|
|
|
30
|
-
# Global SharePoint context
|
|
31
|
+
# Global SharePoint context, Graph API client, and authenticator
|
|
31
32
|
ctx: Optional[ClientContext] = None
|
|
33
|
+
graph_client: Optional[GraphAPIClient] = None
|
|
34
|
+
authenticator = None
|
|
32
35
|
|
|
33
36
|
|
|
34
37
|
def ensure_context(func):
|
|
35
|
-
"""Decorator to ensure SharePoint context
|
|
38
|
+
"""Decorator to ensure SharePoint context and Graph API client are available"""
|
|
36
39
|
@wraps(func)
|
|
37
40
|
async def wrapper(*args, **kwargs):
|
|
38
|
-
global ctx
|
|
41
|
+
global ctx, graph_client, authenticator
|
|
39
42
|
if ctx is None:
|
|
40
43
|
try:
|
|
41
44
|
ctx = create_sharepoint_context()
|
|
42
45
|
logger.info("SharePoint context initialized successfully")
|
|
46
|
+
|
|
47
|
+
# Get site URL for Graph client
|
|
48
|
+
site_url = os.getenv("SHP_SITE_URL")
|
|
49
|
+
|
|
50
|
+
# Create Graph API client
|
|
51
|
+
# The token callback will use the same MSAL authenticator internally
|
|
52
|
+
def token_callback():
|
|
53
|
+
"""Simple token callback that gets a fresh token using MSAL"""
|
|
54
|
+
from .auth import SharePointAuthenticator
|
|
55
|
+
from urllib.parse import urlparse
|
|
56
|
+
|
|
57
|
+
site_url = os.getenv("SHP_SITE_URL")
|
|
58
|
+
client_id = os.getenv("SHP_ID_APP")
|
|
59
|
+
client_secret = os.getenv("SHP_ID_APP_SECRET")
|
|
60
|
+
tenant_id = os.getenv("SHP_TENANT_ID")
|
|
61
|
+
cloud = "government" if ".sharepoint.us" in site_url else "commercial"
|
|
62
|
+
|
|
63
|
+
import msal
|
|
64
|
+
from office365.runtime.auth.token_response import TokenResponse
|
|
65
|
+
|
|
66
|
+
# Build authority URL
|
|
67
|
+
if cloud in ("government", "us"):
|
|
68
|
+
authority_url = f"https://login.microsoftonline.us/{tenant_id}"
|
|
69
|
+
else:
|
|
70
|
+
authority_url = f"https://login.microsoftonline.com/{tenant_id}"
|
|
71
|
+
|
|
72
|
+
# Create MSAL app
|
|
73
|
+
msal_app = msal.ConfidentialClientApplication(
|
|
74
|
+
authority=authority_url,
|
|
75
|
+
client_id=client_id,
|
|
76
|
+
client_credential=client_secret,
|
|
77
|
+
validate_authority=False if cloud in ("government", "us") else True
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Get scope
|
|
81
|
+
parsed = urlparse(site_url)
|
|
82
|
+
sharepoint_root = f"{parsed.scheme}://{parsed.netloc}"
|
|
83
|
+
scopes = [f"{sharepoint_root}/.default"]
|
|
84
|
+
|
|
85
|
+
# Acquire token
|
|
86
|
+
result = msal_app.acquire_token_for_client(scopes=scopes)
|
|
87
|
+
return TokenResponse.from_json(result)
|
|
88
|
+
|
|
89
|
+
graph_client = GraphAPIClient(
|
|
90
|
+
site_url=site_url,
|
|
91
|
+
token_callback=token_callback
|
|
92
|
+
)
|
|
93
|
+
logger.info("Graph API client initialized successfully")
|
|
94
|
+
|
|
43
95
|
except Exception as e:
|
|
44
96
|
logger.error(f"Failed to initialize SharePoint context: {e}")
|
|
45
97
|
raise RuntimeError(
|
|
@@ -324,44 +376,85 @@ async def test_connection() -> list[TextContent]:
|
|
|
324
376
|
|
|
325
377
|
async def list_folders(folder_path: str = "") -> list[TextContent]:
|
|
326
378
|
"""List folders in specified path"""
|
|
379
|
+
doc_lib = get_document_library_path()
|
|
380
|
+
full_path = f"{doc_lib}/{folder_path}" if folder_path else doc_lib
|
|
381
|
+
|
|
327
382
|
try:
|
|
328
|
-
|
|
329
|
-
full_path = f"{doc_lib}/{folder_path}" if folder_path else doc_lib
|
|
330
|
-
|
|
383
|
+
# Try SharePoint REST API first
|
|
331
384
|
folder = ctx.web.get_folder_by_server_relative_path(full_path)
|
|
332
385
|
folders = folder.folders.get().execute_query()
|
|
333
|
-
|
|
386
|
+
|
|
334
387
|
folder_list = []
|
|
335
388
|
for f in folders:
|
|
336
389
|
folder_list.append(f"📁 {f.name}")
|
|
337
|
-
|
|
390
|
+
|
|
338
391
|
result = f"Folders in '{full_path}':\n\n" + "\n".join(folder_list) if folder_list else f"No folders found in '{full_path}'"
|
|
339
|
-
|
|
340
392
|
return [TextContent(type="text", text=result)]
|
|
341
|
-
|
|
393
|
+
|
|
342
394
|
except Exception as e:
|
|
395
|
+
error_msg = str(e).lower()
|
|
396
|
+
|
|
397
|
+
# Check if it's an app-only token error
|
|
398
|
+
if "unsupported app only token" in error_msg or "401" in error_msg:
|
|
399
|
+
logger.warning(f"SharePoint REST API failed with app-only token error, falling back to Graph API")
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
# Fallback to Graph API
|
|
403
|
+
folders = await asyncio.to_thread(graph_client.list_folders, folder_path)
|
|
404
|
+
|
|
405
|
+
folder_list = [f"📁 {f['name']}" for f in folders]
|
|
406
|
+
|
|
407
|
+
result = f"Folders in '{full_path}' (via Graph API):\n\n" + "\n".join(folder_list) if folder_list else f"No folders found in '{full_path}'"
|
|
408
|
+
return [TextContent(type="text", text=result)]
|
|
409
|
+
|
|
410
|
+
except Exception as graph_error:
|
|
411
|
+
return [TextContent(type="text", text=f"Error with both APIs - REST: {e}, Graph: {graph_error}")]
|
|
412
|
+
|
|
413
|
+
# Other errors
|
|
343
414
|
return [TextContent(type="text", text=f"Error listing folders: {str(e)}")]
|
|
344
415
|
|
|
345
416
|
|
|
346
417
|
async def list_documents(folder_path: str = "") -> list[TextContent]:
|
|
347
418
|
"""List documents in specified folder"""
|
|
419
|
+
doc_lib = get_document_library_path()
|
|
420
|
+
full_path = f"{doc_lib}/{folder_path}" if folder_path else doc_lib
|
|
421
|
+
|
|
348
422
|
try:
|
|
349
|
-
|
|
350
|
-
full_path = f"{doc_lib}/{folder_path}" if folder_path else doc_lib
|
|
351
|
-
|
|
423
|
+
# Try SharePoint REST API first
|
|
352
424
|
folder = ctx.web.get_folder_by_server_relative_path(full_path)
|
|
353
425
|
files = folder.files.get().execute_query()
|
|
354
|
-
|
|
426
|
+
|
|
355
427
|
file_list = []
|
|
356
428
|
for f in files:
|
|
357
429
|
size_kb = f.length / 1024
|
|
358
430
|
file_list.append(f"📄 {f.name} ({size_kb:.2f} KB)")
|
|
359
|
-
|
|
431
|
+
|
|
360
432
|
result = f"Documents in '{full_path}':\n\n" + "\n".join(file_list) if file_list else f"No documents found in '{full_path}'"
|
|
361
|
-
|
|
362
433
|
return [TextContent(type="text", text=result)]
|
|
363
|
-
|
|
434
|
+
|
|
364
435
|
except Exception as e:
|
|
436
|
+
error_msg = str(e).lower()
|
|
437
|
+
|
|
438
|
+
# Check if it's an app-only token error
|
|
439
|
+
if "unsupported app only token" in error_msg or "401" in error_msg:
|
|
440
|
+
logger.warning(f"SharePoint REST API failed with app-only token error, falling back to Graph API")
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
# Fallback to Graph API
|
|
444
|
+
files = await asyncio.to_thread(graph_client.list_documents, folder_path)
|
|
445
|
+
|
|
446
|
+
file_list = []
|
|
447
|
+
for f in files:
|
|
448
|
+
size_kb = f['size'] / 1024
|
|
449
|
+
file_list.append(f"📄 {f['name']} ({size_kb:.2f} KB)")
|
|
450
|
+
|
|
451
|
+
result = f"Documents in '{full_path}' (via Graph API):\n\n" + "\n".join(file_list) if file_list else f"No documents found in '{full_path}'"
|
|
452
|
+
return [TextContent(type="text", text=result)]
|
|
453
|
+
|
|
454
|
+
except Exception as graph_error:
|
|
455
|
+
return [TextContent(type="text", text=f"Error with both APIs - REST: {e}, Graph: {graph_error}")]
|
|
456
|
+
|
|
457
|
+
# Other errors
|
|
365
458
|
return [TextContent(type="text", text=f"Error listing documents: {str(e)}")]
|
|
366
459
|
|
|
367
460
|
|
mcp_sharepoint/auth.py
CHANGED
|
@@ -30,8 +30,9 @@ def _patch_datetime_bug():
|
|
|
30
30
|
# Store the original __init__
|
|
31
31
|
original_init = authentication_context.AuthenticationContext.__init__
|
|
32
32
|
|
|
33
|
-
def patched_init(self,
|
|
34
|
-
|
|
33
|
+
def patched_init(self, *args, **kwargs):
|
|
34
|
+
# Call original init with all arguments
|
|
35
|
+
original_init(self, *args, **kwargs)
|
|
35
36
|
# Make token_expires timezone-aware to prevent comparison errors
|
|
36
37
|
if hasattr(self, '_token_expires') and self._token_expires is not None:
|
|
37
38
|
if self._token_expires.tzinfo is None:
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Microsoft Graph API implementation for SharePoint operations
|
|
3
|
+
Used as a fallback when SharePoint REST API doesn't support app-only tokens
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import logging
|
|
7
|
+
import asyncio
|
|
8
|
+
from typing import Optional, Dict, Any, List
|
|
9
|
+
from urllib.parse import urlparse, quote
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GraphAPIClient:
|
|
16
|
+
"""
|
|
17
|
+
Microsoft Graph API client for SharePoint operations.
|
|
18
|
+
Fallback for when SharePoint REST API doesn't support app-only authentication.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, site_url: str, token_callback):
|
|
22
|
+
"""
|
|
23
|
+
Initialize Graph API client.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
site_url: SharePoint site URL (e.g., https://tenant.sharepoint.us/sites/SiteName)
|
|
27
|
+
token_callback: Function that returns access token
|
|
28
|
+
"""
|
|
29
|
+
self.site_url = site_url.rstrip("/")
|
|
30
|
+
self.token_callback = token_callback
|
|
31
|
+
self._site_id = None
|
|
32
|
+
|
|
33
|
+
# Determine Graph API endpoint based on cloud
|
|
34
|
+
if ".sharepoint.us" in site_url:
|
|
35
|
+
self.graph_endpoint = "https://graph.microsoft.us/v1.0"
|
|
36
|
+
logger.info("Using Microsoft Graph US Government endpoint")
|
|
37
|
+
else:
|
|
38
|
+
self.graph_endpoint = "https://graph.microsoft.com/v1.0"
|
|
39
|
+
logger.info("Using Microsoft Graph Commercial endpoint")
|
|
40
|
+
|
|
41
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
42
|
+
"""Get authorization headers with access token."""
|
|
43
|
+
token_obj = self.token_callback()
|
|
44
|
+
# Handle both TokenResponse objects and plain strings
|
|
45
|
+
if hasattr(token_obj, 'accessToken'):
|
|
46
|
+
token = token_obj.accessToken
|
|
47
|
+
else:
|
|
48
|
+
token = str(token_obj)
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
"Authorization": f"Bearer {token}",
|
|
52
|
+
"Accept": "application/json",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def _get_site_id(self) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Get the site ID from the site URL.
|
|
58
|
+
Caches the result for reuse.
|
|
59
|
+
"""
|
|
60
|
+
if self._site_id:
|
|
61
|
+
return self._site_id
|
|
62
|
+
|
|
63
|
+
parsed = urlparse(self.site_url)
|
|
64
|
+
hostname = parsed.netloc
|
|
65
|
+
path = parsed.path.strip("/")
|
|
66
|
+
|
|
67
|
+
# For root site: https://tenant.sharepoint.us
|
|
68
|
+
if not path or path == "sites":
|
|
69
|
+
url = f"{self.graph_endpoint}/sites/{hostname}"
|
|
70
|
+
# For subsite: https://tenant.sharepoint.us/sites/SiteName
|
|
71
|
+
else:
|
|
72
|
+
url = f"{self.graph_endpoint}/sites/{hostname}:/{path}"
|
|
73
|
+
|
|
74
|
+
response = requests.get(url, headers=self._get_headers())
|
|
75
|
+
response.raise_for_status()
|
|
76
|
+
|
|
77
|
+
self._site_id = response.json()["id"]
|
|
78
|
+
logger.info(f"Retrieved site ID: {self._site_id}")
|
|
79
|
+
return self._site_id
|
|
80
|
+
|
|
81
|
+
def _get_drive_id(self) -> str:
|
|
82
|
+
"""Get the default document library drive ID."""
|
|
83
|
+
site_id = self._get_site_id()
|
|
84
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drive"
|
|
85
|
+
|
|
86
|
+
response = requests.get(url, headers=self._get_headers())
|
|
87
|
+
response.raise_for_status()
|
|
88
|
+
|
|
89
|
+
drive_id = response.json()["id"]
|
|
90
|
+
logger.info(f"Retrieved drive ID: {drive_id}")
|
|
91
|
+
return drive_id
|
|
92
|
+
|
|
93
|
+
def list_folders(self, folder_path: str = "") -> List[Dict[str, Any]]:
|
|
94
|
+
"""
|
|
95
|
+
List folders in the specified path.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
folder_path: Relative path from document library root
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List of folder objects with name, id, webUrl
|
|
102
|
+
"""
|
|
103
|
+
site_id = self._get_site_id()
|
|
104
|
+
drive_id = self._get_drive_id()
|
|
105
|
+
|
|
106
|
+
if folder_path:
|
|
107
|
+
# URL encode the path
|
|
108
|
+
encoded_path = quote(folder_path)
|
|
109
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root:/{encoded_path}:/children"
|
|
110
|
+
else:
|
|
111
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root/children"
|
|
112
|
+
|
|
113
|
+
response = requests.get(url, headers=self._get_headers())
|
|
114
|
+
response.raise_for_status()
|
|
115
|
+
|
|
116
|
+
items = response.json().get("value", [])
|
|
117
|
+
# Filter to only folders
|
|
118
|
+
folders = [
|
|
119
|
+
{
|
|
120
|
+
"name": item["name"],
|
|
121
|
+
"id": item["id"],
|
|
122
|
+
"webUrl": item.get("webUrl", ""),
|
|
123
|
+
}
|
|
124
|
+
for item in items
|
|
125
|
+
if "folder" in item
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
logger.info(f"Found {len(folders)} folders in '{folder_path}'")
|
|
129
|
+
return folders
|
|
130
|
+
|
|
131
|
+
def list_documents(self, folder_path: str = "") -> List[Dict[str, Any]]:
|
|
132
|
+
"""
|
|
133
|
+
List documents in the specified folder.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
folder_path: Relative path from document library root
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
List of file objects with name, id, size, webUrl
|
|
140
|
+
"""
|
|
141
|
+
site_id = self._get_site_id()
|
|
142
|
+
drive_id = self._get_drive_id()
|
|
143
|
+
|
|
144
|
+
if folder_path:
|
|
145
|
+
encoded_path = quote(folder_path)
|
|
146
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root:/{encoded_path}:/children"
|
|
147
|
+
else:
|
|
148
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root/children"
|
|
149
|
+
|
|
150
|
+
response = requests.get(url, headers=self._get_headers())
|
|
151
|
+
response.raise_for_status()
|
|
152
|
+
|
|
153
|
+
items = response.json().get("value", [])
|
|
154
|
+
# Filter to only files
|
|
155
|
+
files = [
|
|
156
|
+
{
|
|
157
|
+
"name": item["name"],
|
|
158
|
+
"id": item["id"],
|
|
159
|
+
"size": item.get("size", 0),
|
|
160
|
+
"webUrl": item.get("webUrl", ""),
|
|
161
|
+
}
|
|
162
|
+
for item in items
|
|
163
|
+
if "file" in item
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
logger.info(f"Found {len(files)} files in '{folder_path}'")
|
|
167
|
+
return files
|
|
168
|
+
|
|
169
|
+
def get_file_content(self, file_path: str) -> bytes:
|
|
170
|
+
"""
|
|
171
|
+
Get the content of a file.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
file_path: Relative path to the file
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
File content as bytes
|
|
178
|
+
"""
|
|
179
|
+
site_id = self._get_site_id()
|
|
180
|
+
drive_id = self._get_drive_id()
|
|
181
|
+
|
|
182
|
+
encoded_path = quote(file_path)
|
|
183
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root:/{encoded_path}:/content"
|
|
184
|
+
|
|
185
|
+
response = requests.get(url, headers=self._get_headers())
|
|
186
|
+
response.raise_for_status()
|
|
187
|
+
|
|
188
|
+
logger.info(f"Retrieved content for '{file_path}' ({len(response.content)} bytes)")
|
|
189
|
+
return response.content
|
|
190
|
+
|
|
191
|
+
def upload_file(self, folder_path: str, file_name: str, content: bytes) -> Dict[str, Any]:
|
|
192
|
+
"""
|
|
193
|
+
Upload a file to SharePoint.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
folder_path: Destination folder path
|
|
197
|
+
file_name: Name of the file
|
|
198
|
+
content: File content as bytes
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
File metadata
|
|
202
|
+
"""
|
|
203
|
+
site_id = self._get_site_id()
|
|
204
|
+
drive_id = self._get_drive_id()
|
|
205
|
+
|
|
206
|
+
if folder_path:
|
|
207
|
+
full_path = f"{folder_path}/{file_name}"
|
|
208
|
+
else:
|
|
209
|
+
full_path = file_name
|
|
210
|
+
|
|
211
|
+
encoded_path = quote(full_path)
|
|
212
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root:/{encoded_path}:/content"
|
|
213
|
+
|
|
214
|
+
headers = self._get_headers()
|
|
215
|
+
headers["Content-Type"] = "application/octet-stream"
|
|
216
|
+
|
|
217
|
+
response = requests.put(url, headers=headers, data=content)
|
|
218
|
+
response.raise_for_status()
|
|
219
|
+
|
|
220
|
+
logger.info(f"Uploaded '{file_name}' to '{folder_path}'")
|
|
221
|
+
return response.json()
|
|
222
|
+
|
|
223
|
+
def delete_file(self, file_path: str) -> None:
|
|
224
|
+
"""
|
|
225
|
+
Delete a file.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
file_path: Relative path to the file
|
|
229
|
+
"""
|
|
230
|
+
site_id = self._get_site_id()
|
|
231
|
+
drive_id = self._get_drive_id()
|
|
232
|
+
|
|
233
|
+
encoded_path = quote(file_path)
|
|
234
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root:/{encoded_path}"
|
|
235
|
+
|
|
236
|
+
response = requests.delete(url, headers=self._get_headers())
|
|
237
|
+
response.raise_for_status()
|
|
238
|
+
|
|
239
|
+
logger.info(f"Deleted '{file_path}'")
|
|
240
|
+
|
|
241
|
+
def create_folder(self, parent_path: str, folder_name: str) -> Dict[str, Any]:
|
|
242
|
+
"""
|
|
243
|
+
Create a new folder.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
parent_path: Path to parent folder
|
|
247
|
+
folder_name: Name of the new folder
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Folder metadata
|
|
251
|
+
"""
|
|
252
|
+
site_id = self._get_site_id()
|
|
253
|
+
drive_id = self._get_drive_id()
|
|
254
|
+
|
|
255
|
+
if parent_path:
|
|
256
|
+
encoded_path = quote(parent_path)
|
|
257
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root:/{encoded_path}:/children"
|
|
258
|
+
else:
|
|
259
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root/children"
|
|
260
|
+
|
|
261
|
+
payload = {
|
|
262
|
+
"name": folder_name,
|
|
263
|
+
"folder": {},
|
|
264
|
+
"@microsoft.graph.conflictBehavior": "fail"
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
response = requests.post(url, headers=self._get_headers(), json=payload)
|
|
268
|
+
response.raise_for_status()
|
|
269
|
+
|
|
270
|
+
logger.info(f"Created folder '{folder_name}' in '{parent_path}'")
|
|
271
|
+
return response.json()
|
|
272
|
+
|
|
273
|
+
def delete_folder(self, folder_path: str) -> None:
|
|
274
|
+
"""
|
|
275
|
+
Delete a folder.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
folder_path: Relative path to the folder
|
|
279
|
+
"""
|
|
280
|
+
site_id = self._get_site_id()
|
|
281
|
+
drive_id = self._get_drive_id()
|
|
282
|
+
|
|
283
|
+
encoded_path = quote(folder_path)
|
|
284
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root:/{encoded_path}"
|
|
285
|
+
|
|
286
|
+
response = requests.delete(url, headers=self._get_headers())
|
|
287
|
+
response.raise_for_status()
|
|
288
|
+
|
|
289
|
+
logger.info(f"Deleted folder '{folder_path}'")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-sharepoint-us
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.12
|
|
4
4
|
Summary: SharePoint MCP Server with Modern Azure AD Authentication
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/mdev26/mcp-sharepoint-us
|
|
@@ -22,6 +22,7 @@ Requires-Dist: office365-rest-python-client>=2.5.0
|
|
|
22
22
|
Requires-Dist: msal>=1.24.0
|
|
23
23
|
Requires-Dist: python-dotenv>=1.0.0
|
|
24
24
|
Requires-Dist: pydantic>=2.0.0
|
|
25
|
+
Requires-Dist: requests>=2.31.0
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
27
28
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
mcp_sharepoint/__init__.py,sha256=bPS8QLq2U83JjAIr76_cEwQfZkv92K8MMWR7hbuo-9s,24298
|
|
2
|
+
mcp_sharepoint/__main__.py,sha256=4iVDdDZx4rQ4Zo-x0RaCrT-NKeGObIz_ks3YF8di2nA,132
|
|
3
|
+
mcp_sharepoint/auth.py,sha256=Tve5y-m1WwL6eTVpofeDv3zFSIDhwo1s26gJSy3F_1s,14729
|
|
4
|
+
mcp_sharepoint/graph_api.py,sha256=63ZCx4G5BqimkYcYbibJtRYiU2UhsjN8nXp-qzPGBfA,9273
|
|
5
|
+
mcp_sharepoint_us-2.0.12.dist-info/licenses/LICENSE,sha256=SRM8juGH4GjIqnl5rrp-P-S5mW5h2mINOPx5-wOZG6s,1112
|
|
6
|
+
mcp_sharepoint_us-2.0.12.dist-info/METADATA,sha256=UP72g9PzIBBNJFY_Se2ECqiuocwUhj8aiwEu7ur1yY0,11413
|
|
7
|
+
mcp_sharepoint_us-2.0.12.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
8
|
+
mcp_sharepoint_us-2.0.12.dist-info/entry_points.txt,sha256=UZOa_7OLI41rmsErbvnSz9RahPMGQVcqZUFMphOcjbY,57
|
|
9
|
+
mcp_sharepoint_us-2.0.12.dist-info/top_level.txt,sha256=R6mRoWe61lz4kUSKGV6S2XVbE7825xfC_J-ouZIYpuo,15
|
|
10
|
+
mcp_sharepoint_us-2.0.12.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
mcp_sharepoint/__init__.py,sha256=h13i3__OCM4wXMWMGcmCulcMcOhjhXbBuQ4p-SepCUo,20013
|
|
2
|
-
mcp_sharepoint/__main__.py,sha256=4iVDdDZx4rQ4Zo-x0RaCrT-NKeGObIz_ks3YF8di2nA,132
|
|
3
|
-
mcp_sharepoint/auth.py,sha256=boN1frfUnu4H1zbb-LOWSeyX8_9GnCW_eiS78Gv6VyM,14653
|
|
4
|
-
mcp_sharepoint_us-2.0.10.dist-info/licenses/LICENSE,sha256=SRM8juGH4GjIqnl5rrp-P-S5mW5h2mINOPx5-wOZG6s,1112
|
|
5
|
-
mcp_sharepoint_us-2.0.10.dist-info/METADATA,sha256=knhH2uJm6LEYKHiatWGpgE-7OA4ybWp33BtWC9-fFpk,11380
|
|
6
|
-
mcp_sharepoint_us-2.0.10.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
7
|
-
mcp_sharepoint_us-2.0.10.dist-info/entry_points.txt,sha256=UZOa_7OLI41rmsErbvnSz9RahPMGQVcqZUFMphOcjbY,57
|
|
8
|
-
mcp_sharepoint_us-2.0.10.dist-info/top_level.txt,sha256=R6mRoWe61lz4kUSKGV6S2XVbE7825xfC_J-ouZIYpuo,15
|
|
9
|
-
mcp_sharepoint_us-2.0.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|