mcp-sharepoint-us 2.0.11__tar.gz → 2.0.13__tar.gz
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_us-2.0.11 → mcp_sharepoint_us-2.0.13}/PKG-INFO +3 -2
- {mcp_sharepoint_us-2.0.11 → mcp_sharepoint_us-2.0.13}/pyproject.toml +3 -2
- {mcp_sharepoint_us-2.0.11 → mcp_sharepoint_us-2.0.13}/src/mcp_sharepoint/__init__.py +114 -116
- {mcp_sharepoint_us-2.0.11 → mcp_sharepoint_us-2.0.13}/src/mcp_sharepoint/auth.py +91 -18
- mcp_sharepoint_us-2.0.13/src/mcp_sharepoint/graph_api.py +328 -0
- {mcp_sharepoint_us-2.0.11 → mcp_sharepoint_us-2.0.13}/src/mcp_sharepoint_us.egg-info/PKG-INFO +3 -2
- {mcp_sharepoint_us-2.0.11 → mcp_sharepoint_us-2.0.13}/src/mcp_sharepoint_us.egg-info/SOURCES.txt +1 -0
- {mcp_sharepoint_us-2.0.11 → mcp_sharepoint_us-2.0.13}/src/mcp_sharepoint_us.egg-info/requires.txt +1 -0
- {mcp_sharepoint_us-2.0.11 → mcp_sharepoint_us-2.0.13}/LICENSE +0 -0
- {mcp_sharepoint_us-2.0.11 → mcp_sharepoint_us-2.0.13}/README.md +0 -0
- {mcp_sharepoint_us-2.0.11 → mcp_sharepoint_us-2.0.13}/setup.cfg +0 -0
- {mcp_sharepoint_us-2.0.11 → mcp_sharepoint_us-2.0.13}/src/mcp_sharepoint/__main__.py +0 -0
- {mcp_sharepoint_us-2.0.11 → mcp_sharepoint_us-2.0.13}/src/mcp_sharepoint_us.egg-info/dependency_links.txt +0 -0
- {mcp_sharepoint_us-2.0.11 → mcp_sharepoint_us-2.0.13}/src/mcp_sharepoint_us.egg-info/entry_points.txt +0 -0
- {mcp_sharepoint_us-2.0.11 → mcp_sharepoint_us-2.0.13}/src/mcp_sharepoint_us.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-sharepoint-us
|
|
3
|
-
Version: 2.0.
|
|
4
|
-
Summary: SharePoint MCP Server with
|
|
3
|
+
Version: 2.0.13
|
|
4
|
+
Summary: SharePoint MCP Server with Microsoft Graph API
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/mdev26/mcp-sharepoint-us
|
|
7
7
|
Project-URL: Repository, 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"
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mcp-sharepoint-us"
|
|
7
|
-
version = "2.0.
|
|
8
|
-
description = "SharePoint MCP Server with
|
|
7
|
+
version = "2.0.13"
|
|
8
|
+
description = "SharePoint MCP Server with Microsoft Graph API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = { text = "MIT" }
|
|
@@ -26,6 +26,7 @@ dependencies = [
|
|
|
26
26
|
"msal>=1.24.0",
|
|
27
27
|
"python-dotenv>=1.0.0",
|
|
28
28
|
"pydantic>=2.0.0",
|
|
29
|
+
"requests>=2.31.0",
|
|
29
30
|
]
|
|
30
31
|
|
|
31
32
|
[project.optional-dependencies]
|
|
@@ -14,11 +14,7 @@ from mcp.types import Resource, Tool, TextContent, ImageContent, EmbeddedResourc
|
|
|
14
14
|
from pydantic import AnyUrl
|
|
15
15
|
import mcp.server.stdio
|
|
16
16
|
|
|
17
|
-
from
|
|
18
|
-
from office365.sharepoint.folders.folder import Folder
|
|
19
|
-
from office365.sharepoint.client_context import ClientContext
|
|
20
|
-
|
|
21
|
-
from .auth import create_sharepoint_context
|
|
17
|
+
from .graph_api import GraphAPIClient
|
|
22
18
|
|
|
23
19
|
# Setup logging
|
|
24
20
|
logging.basicConfig(level=logging.INFO)
|
|
@@ -27,27 +23,55 @@ logger = logging.getLogger(__name__)
|
|
|
27
23
|
# Initialize MCP server
|
|
28
24
|
app = Server("mcp-sharepoint")
|
|
29
25
|
|
|
30
|
-
# Global
|
|
31
|
-
|
|
26
|
+
# Global Graph API client and authenticator
|
|
27
|
+
graph_client: Optional[GraphAPIClient] = None
|
|
28
|
+
authenticator = None
|
|
32
29
|
|
|
33
30
|
|
|
34
31
|
def ensure_context(func):
|
|
35
|
-
"""Decorator to ensure
|
|
32
|
+
"""Decorator to ensure Graph API client is available"""
|
|
36
33
|
@wraps(func)
|
|
37
34
|
async def wrapper(*args, **kwargs):
|
|
38
|
-
global
|
|
39
|
-
if
|
|
35
|
+
global graph_client, authenticator
|
|
36
|
+
if graph_client is None:
|
|
40
37
|
try:
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
from .auth import SharePointAuthenticator
|
|
39
|
+
|
|
40
|
+
# Get credentials
|
|
41
|
+
site_url = os.getenv("SHP_SITE_URL")
|
|
42
|
+
client_id = os.getenv("SHP_ID_APP")
|
|
43
|
+
client_secret = os.getenv("SHP_ID_APP_SECRET")
|
|
44
|
+
tenant_id = os.getenv("SHP_TENANT_ID")
|
|
45
|
+
cloud = "government" if ".sharepoint.us" in site_url else "commercial"
|
|
46
|
+
|
|
47
|
+
# Create shared authenticator
|
|
48
|
+
authenticator = SharePointAuthenticator(
|
|
49
|
+
site_url=site_url,
|
|
50
|
+
client_id=client_id,
|
|
51
|
+
client_secret=client_secret,
|
|
52
|
+
tenant_id=tenant_id,
|
|
53
|
+
cloud=cloud
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Create Graph API client with direct token access
|
|
57
|
+
def get_token():
|
|
58
|
+
"""Get access token for Graph API"""
|
|
59
|
+
return authenticator.get_access_token()
|
|
60
|
+
|
|
61
|
+
graph_client = GraphAPIClient(
|
|
62
|
+
site_url=site_url,
|
|
63
|
+
token_callback=get_token
|
|
64
|
+
)
|
|
65
|
+
logger.info("Graph API client initialized successfully")
|
|
66
|
+
|
|
43
67
|
except Exception as e:
|
|
44
|
-
logger.error(f"Failed to initialize
|
|
68
|
+
logger.error(f"Failed to initialize Graph API client: {e}")
|
|
45
69
|
raise RuntimeError(
|
|
46
|
-
f"
|
|
70
|
+
f"Graph API authentication failed: {e}. "
|
|
47
71
|
"Please check your environment variables and ensure:\n"
|
|
48
72
|
"1. SHP_TENANT_ID is set correctly\n"
|
|
49
|
-
"2. Your Azure AD app has
|
|
50
|
-
"3.
|
|
73
|
+
"2. Your Azure AD app has Microsoft Graph API permissions\n"
|
|
74
|
+
"3. The app registration has 'Sites.Read.All' and 'Files.ReadWrite.All' permissions"
|
|
51
75
|
)
|
|
52
76
|
return await func(*args, **kwargs)
|
|
53
77
|
return wrapper
|
|
@@ -323,60 +347,48 @@ async def test_connection() -> list[TextContent]:
|
|
|
323
347
|
|
|
324
348
|
|
|
325
349
|
async def list_folders(folder_path: str = "") -> list[TextContent]:
|
|
326
|
-
"""List folders in specified path"""
|
|
350
|
+
"""List folders in specified path using Microsoft Graph API"""
|
|
351
|
+
doc_lib = get_document_library_path()
|
|
352
|
+
full_path = f"{doc_lib}/{folder_path}" if folder_path else doc_lib
|
|
353
|
+
|
|
327
354
|
try:
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
folders = folder.folders.get().execute_query()
|
|
333
|
-
|
|
334
|
-
folder_list = []
|
|
335
|
-
for f in folders:
|
|
336
|
-
folder_list.append(f"📁 {f.name}")
|
|
337
|
-
|
|
355
|
+
# Use Graph API directly
|
|
356
|
+
folders = await asyncio.to_thread(graph_client.list_folders, folder_path)
|
|
357
|
+
folder_list = [f"📁 {f['name']}" for f in folders]
|
|
358
|
+
|
|
338
359
|
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
360
|
return [TextContent(type="text", text=result)]
|
|
341
|
-
|
|
361
|
+
|
|
342
362
|
except Exception as e:
|
|
343
363
|
return [TextContent(type="text", text=f"Error listing folders: {str(e)}")]
|
|
344
364
|
|
|
345
365
|
|
|
346
366
|
async def list_documents(folder_path: str = "") -> list[TextContent]:
|
|
347
|
-
"""List documents in specified folder"""
|
|
367
|
+
"""List documents in specified folder using Microsoft Graph API"""
|
|
368
|
+
doc_lib = get_document_library_path()
|
|
369
|
+
full_path = f"{doc_lib}/{folder_path}" if folder_path else doc_lib
|
|
370
|
+
|
|
348
371
|
try:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
folder = ctx.web.get_folder_by_server_relative_path(full_path)
|
|
353
|
-
files = folder.files.get().execute_query()
|
|
354
|
-
|
|
372
|
+
# Use Graph API directly
|
|
373
|
+
files = await asyncio.to_thread(graph_client.list_documents, folder_path)
|
|
374
|
+
|
|
355
375
|
file_list = []
|
|
356
376
|
for f in files:
|
|
357
|
-
size_kb = f
|
|
358
|
-
file_list.append(f"📄 {f
|
|
359
|
-
|
|
377
|
+
size_kb = f['size'] / 1024
|
|
378
|
+
file_list.append(f"📄 {f['name']} ({size_kb:.2f} KB)")
|
|
379
|
+
|
|
360
380
|
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
381
|
return [TextContent(type="text", text=result)]
|
|
363
|
-
|
|
382
|
+
|
|
364
383
|
except Exception as e:
|
|
365
384
|
return [TextContent(type="text", text=f"Error listing documents: {str(e)}")]
|
|
366
385
|
|
|
367
386
|
|
|
368
387
|
async def get_document_content(file_path: str) -> list[TextContent]:
|
|
369
|
-
"""Get document content"""
|
|
388
|
+
"""Get document content using Microsoft Graph API"""
|
|
370
389
|
try:
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
def _read_bytes():
|
|
375
|
-
sp_file = ctx.web.get_file_by_server_relative_path(full_path)
|
|
376
|
-
# IMPORTANT: execute the request
|
|
377
|
-
return sp_file.read().execute_query()
|
|
378
|
-
|
|
379
|
-
content = await asyncio.to_thread(_read_bytes)
|
|
390
|
+
# Use Graph API to get file content
|
|
391
|
+
content = await asyncio.to_thread(graph_client.get_file_content, file_path)
|
|
380
392
|
|
|
381
393
|
ext = os.path.splitext(file_path)[1].lower()
|
|
382
394
|
text_extensions = {'.txt', '.md', '.json', '.xml', '.html', '.csv', '.log'}
|
|
@@ -401,141 +413,127 @@ async def get_document_content(file_path: str) -> list[TextContent]:
|
|
|
401
413
|
|
|
402
414
|
|
|
403
415
|
async def upload_document(folder_path: str, file_name: str, content: str, is_binary: bool = False) -> list[TextContent]:
|
|
404
|
-
"""Upload a document"""
|
|
416
|
+
"""Upload a document using Microsoft Graph API"""
|
|
405
417
|
try:
|
|
406
|
-
doc_lib = get_document_library_path()
|
|
407
|
-
full_path = f"{doc_lib}/{folder_path}" if folder_path else doc_lib
|
|
408
|
-
|
|
409
|
-
folder = ctx.web.get_folder_by_server_relative_path(full_path)
|
|
410
|
-
|
|
411
418
|
if is_binary:
|
|
412
419
|
file_content = base64.b64decode(content)
|
|
413
420
|
else:
|
|
414
421
|
file_content = content.encode('utf-8')
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
422
|
+
|
|
423
|
+
# Use Graph API to upload file
|
|
424
|
+
result = await asyncio.to_thread(
|
|
425
|
+
graph_client.upload_file,
|
|
426
|
+
folder_path,
|
|
427
|
+
file_name,
|
|
428
|
+
file_content
|
|
429
|
+
)
|
|
430
|
+
|
|
418
431
|
return [TextContent(
|
|
419
432
|
type="text",
|
|
420
|
-
text=f"✓ Successfully uploaded '{file_name}' to '{
|
|
433
|
+
text=f"✓ Successfully uploaded '{file_name}' to '{folder_path or 'root'}'"
|
|
421
434
|
)]
|
|
422
|
-
|
|
435
|
+
|
|
423
436
|
except Exception as e:
|
|
424
437
|
return [TextContent(type="text", text=f"Error uploading document: {str(e)}")]
|
|
425
438
|
|
|
426
439
|
|
|
427
440
|
async def update_document(file_path: str, content: str, is_binary: bool = False) -> list[TextContent]:
|
|
428
|
-
"""Update a document"""
|
|
441
|
+
"""Update a document using Microsoft Graph API"""
|
|
429
442
|
try:
|
|
430
|
-
doc_lib = get_document_library_path()
|
|
431
|
-
full_path = f"{doc_lib}/{file_path}"
|
|
432
|
-
|
|
433
443
|
if is_binary:
|
|
434
444
|
file_content = base64.b64decode(content)
|
|
435
445
|
else:
|
|
436
446
|
file_content = content.encode('utf-8')
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
447
|
+
|
|
448
|
+
# Split file_path into folder and filename
|
|
449
|
+
folder_path = os.path.dirname(file_path)
|
|
450
|
+
file_name = os.path.basename(file_path)
|
|
451
|
+
|
|
452
|
+
# Use Graph API to upload/update file (PUT overwrites)
|
|
453
|
+
await asyncio.to_thread(
|
|
454
|
+
graph_client.upload_file,
|
|
455
|
+
folder_path,
|
|
456
|
+
file_name,
|
|
457
|
+
file_content
|
|
458
|
+
)
|
|
459
|
+
|
|
441
460
|
return [TextContent(
|
|
442
461
|
type="text",
|
|
443
462
|
text=f"✓ Successfully updated '{file_path}'"
|
|
444
463
|
)]
|
|
445
|
-
|
|
464
|
+
|
|
446
465
|
except Exception as e:
|
|
447
466
|
return [TextContent(type="text", text=f"Error updating document: {str(e)}")]
|
|
448
467
|
|
|
449
468
|
|
|
450
469
|
async def delete_document(file_path: str) -> list[TextContent]:
|
|
451
|
-
"""Delete a document"""
|
|
470
|
+
"""Delete a document using Microsoft Graph API"""
|
|
452
471
|
try:
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
file = ctx.web.get_file_by_server_relative_path(full_path)
|
|
457
|
-
file.delete_object().execute_query()
|
|
458
|
-
|
|
472
|
+
# Use Graph API to delete file
|
|
473
|
+
await asyncio.to_thread(graph_client.delete_file, file_path)
|
|
474
|
+
|
|
459
475
|
return [TextContent(
|
|
460
476
|
type="text",
|
|
461
477
|
text=f"✓ Successfully deleted '{file_path}'"
|
|
462
478
|
)]
|
|
463
|
-
|
|
479
|
+
|
|
464
480
|
except Exception as e:
|
|
465
481
|
return [TextContent(type="text", text=f"Error deleting document: {str(e)}")]
|
|
466
482
|
|
|
467
483
|
|
|
468
484
|
async def create_folder(folder_path: str, folder_name: str) -> list[TextContent]:
|
|
469
|
-
"""Create a folder"""
|
|
485
|
+
"""Create a folder using Microsoft Graph API"""
|
|
470
486
|
try:
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
487
|
+
# Use Graph API to create folder
|
|
488
|
+
await asyncio.to_thread(
|
|
489
|
+
graph_client.create_folder,
|
|
490
|
+
folder_path,
|
|
491
|
+
folder_name
|
|
492
|
+
)
|
|
493
|
+
|
|
477
494
|
return [TextContent(
|
|
478
495
|
type="text",
|
|
479
|
-
text=f"✓ Successfully created folder '{folder_name}' in '{
|
|
496
|
+
text=f"✓ Successfully created folder '{folder_name}' in '{folder_path or 'root'}'"
|
|
480
497
|
)]
|
|
481
|
-
|
|
498
|
+
|
|
482
499
|
except Exception as e:
|
|
483
500
|
return [TextContent(type="text", text=f"Error creating folder: {str(e)}")]
|
|
484
501
|
|
|
485
502
|
|
|
486
503
|
async def delete_folder(folder_path: str) -> list[TextContent]:
|
|
487
|
-
"""Delete a folder"""
|
|
504
|
+
"""Delete a folder using Microsoft Graph API"""
|
|
488
505
|
try:
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
folder = ctx.web.get_folder_by_server_relative_path(full_path)
|
|
493
|
-
folder.delete_object().execute_query()
|
|
494
|
-
|
|
506
|
+
# Use Graph API to delete folder
|
|
507
|
+
await asyncio.to_thread(graph_client.delete_folder, folder_path)
|
|
508
|
+
|
|
495
509
|
return [TextContent(
|
|
496
510
|
type="text",
|
|
497
511
|
text=f"✓ Successfully deleted folder '{folder_path}'"
|
|
498
512
|
)]
|
|
499
|
-
|
|
513
|
+
|
|
500
514
|
except Exception as e:
|
|
501
515
|
return [TextContent(type="text", text=f"Error deleting folder: {str(e)}")]
|
|
502
516
|
|
|
503
517
|
|
|
504
518
|
async def get_tree(folder_path: str = "", max_depth: int = 5, current_depth: int = 0) -> list[TextContent]:
|
|
505
|
-
"""Get folder tree structure"""
|
|
519
|
+
"""Get folder tree structure using Microsoft Graph API"""
|
|
506
520
|
if current_depth >= max_depth:
|
|
507
521
|
return [TextContent(type="text", text="Max depth reached")]
|
|
508
522
|
|
|
509
523
|
try:
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
folder = ctx.web.get_folder_by_server_relative_path(full_path)
|
|
514
|
-
folders = folder.folders.get().execute_query()
|
|
524
|
+
# Use Graph API to list folders
|
|
525
|
+
folders = await asyncio.to_thread(graph_client.list_folders, folder_path)
|
|
515
526
|
|
|
516
527
|
indent = " " * current_depth
|
|
517
528
|
tree_lines = [f"{indent}📁 {folder_path or 'Root'}"]
|
|
518
529
|
|
|
519
530
|
for f in folders:
|
|
520
|
-
sub_path = f"{folder_path}/{f
|
|
531
|
+
sub_path = f"{folder_path}/{f['name']}" if folder_path else f['name']
|
|
521
532
|
sub_tree = await get_tree(sub_path, max_depth, current_depth + 1)
|
|
522
533
|
tree_lines.append(sub_tree[0].text)
|
|
523
534
|
|
|
524
535
|
return [TextContent(type="text", text="\n".join(tree_lines))]
|
|
525
536
|
|
|
526
|
-
except TypeError as e:
|
|
527
|
-
if "can't compare offset-naive and offset-aware datetimes" in str(e):
|
|
528
|
-
logger.error(
|
|
529
|
-
f"DateTime comparison error occurred despite patch. "
|
|
530
|
-
f"This may indicate a new code path in the library. Error: {e}"
|
|
531
|
-
)
|
|
532
|
-
return [TextContent(
|
|
533
|
-
type="text",
|
|
534
|
-
text=f"Encountered a datetime comparison issue. "
|
|
535
|
-
f"A workaround patch is applied, but this specific code path may need attention.\n"
|
|
536
|
-
f"Alternative: Use List_SharePoint_Folders for folder navigation."
|
|
537
|
-
)]
|
|
538
|
-
raise
|
|
539
537
|
except Exception as e:
|
|
540
538
|
return [TextContent(type="text", text=f"Error getting tree: {str(e)}")]
|
|
541
539
|
|
|
@@ -7,7 +7,6 @@ import logging
|
|
|
7
7
|
import time
|
|
8
8
|
import random
|
|
9
9
|
from typing import Optional
|
|
10
|
-
from urllib.parse import urlparse
|
|
11
10
|
from datetime import datetime, timezone
|
|
12
11
|
from office365.sharepoint.client_context import ClientContext
|
|
13
12
|
from office365.runtime.auth.client_credential import ClientCredential
|
|
@@ -85,7 +84,17 @@ class SharePointAuthenticator:
|
|
|
85
84
|
self.cert_path = cert_path
|
|
86
85
|
self.cert_thumbprint = cert_thumbprint
|
|
87
86
|
self.cloud = cloud.lower()
|
|
88
|
-
|
|
87
|
+
|
|
88
|
+
# Initialize token cache
|
|
89
|
+
self._access_token = None
|
|
90
|
+
self._access_token_exp = 0
|
|
91
|
+
|
|
92
|
+
# Set Graph API scope based on cloud environment
|
|
93
|
+
if self.cloud in ("government", "us"):
|
|
94
|
+
self._scopes = ["https://graph.microsoft.us/.default"]
|
|
95
|
+
else:
|
|
96
|
+
self._scopes = ["https://graph.microsoft.com/.default"]
|
|
97
|
+
|
|
89
98
|
def get_context_with_msal(self) -> ClientContext:
|
|
90
99
|
"""
|
|
91
100
|
Get ClientContext using MSAL for modern Azure AD authentication.
|
|
@@ -123,19 +132,7 @@ class SharePointAuthenticator:
|
|
|
123
132
|
self._msal_app = msal.ConfidentialClientApplication(**msal_params)
|
|
124
133
|
self._authority_url = authority_url
|
|
125
134
|
|
|
126
|
-
|
|
127
|
-
# MSAL caches too, but keeping the raw token avoids extra work in Office365 callbacks.
|
|
128
|
-
if not hasattr(self, "_access_token"):
|
|
129
|
-
self._access_token = None
|
|
130
|
-
self._access_token_exp = 0
|
|
131
|
-
|
|
132
|
-
# Extract root SharePoint URL for scope
|
|
133
|
-
# For https://tenant.sharepoint.us/sites/SiteName -> https://tenant.sharepoint.us
|
|
134
|
-
parsed = urlparse(self.site_url)
|
|
135
|
-
sharepoint_root = f"{parsed.scheme}://{parsed.netloc}"
|
|
136
|
-
scopes = [f"{sharepoint_root}/.default"]
|
|
137
|
-
|
|
138
|
-
logger.info(f"Using SharePoint root scope: {sharepoint_root}/.default")
|
|
135
|
+
logger.info(f"Using Graph API scope: {self._scopes[0]}")
|
|
139
136
|
|
|
140
137
|
def acquire_token():
|
|
141
138
|
"""
|
|
@@ -153,7 +150,7 @@ class SharePointAuthenticator:
|
|
|
153
150
|
last_err = None
|
|
154
151
|
for attempt in range(1, 6): # 5 attempts
|
|
155
152
|
try:
|
|
156
|
-
result = self._msal_app.acquire_token_for_client(scopes=
|
|
153
|
+
result = self._msal_app.acquire_token_for_client(scopes=self._scopes)
|
|
157
154
|
|
|
158
155
|
if "access_token" not in result:
|
|
159
156
|
error_desc = result.get("error_description", "Unknown error")
|
|
@@ -161,7 +158,7 @@ class SharePointAuthenticator:
|
|
|
161
158
|
raise ValueError(
|
|
162
159
|
f"Failed to acquire token: {error} - {error_desc}\n"
|
|
163
160
|
f"Authority: {self._authority_url}\n"
|
|
164
|
-
f"Scopes: {
|
|
161
|
+
f"Scopes: {self._scopes}"
|
|
165
162
|
)
|
|
166
163
|
|
|
167
164
|
token = result["access_token"]
|
|
@@ -191,7 +188,83 @@ class SharePointAuthenticator:
|
|
|
191
188
|
logger.info("Successfully authenticated using MSAL (Modern Azure AD)")
|
|
192
189
|
return ctx
|
|
193
190
|
|
|
194
|
-
|
|
191
|
+
def get_access_token(self) -> str:
|
|
192
|
+
"""
|
|
193
|
+
Get access token directly for use with Microsoft Graph API.
|
|
194
|
+
Uses the same retry logic as get_context_with_msal() but returns just the token string.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Access token as string
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
RuntimeError: If token acquisition fails after retries
|
|
201
|
+
"""
|
|
202
|
+
# Initialize MSAL app if not already done
|
|
203
|
+
if not hasattr(self, "_msal_app"):
|
|
204
|
+
if self.cloud in ("government", "us"):
|
|
205
|
+
authority_url = f"https://login.microsoftonline.us/{self.tenant_id}"
|
|
206
|
+
logger.info("Using Azure US Government Cloud endpoints")
|
|
207
|
+
else:
|
|
208
|
+
authority_url = f"https://login.microsoftonline.com/{self.tenant_id}"
|
|
209
|
+
logger.info("Using Azure Commercial Cloud endpoints")
|
|
210
|
+
|
|
211
|
+
self._token_cache = msal.SerializableTokenCache()
|
|
212
|
+
|
|
213
|
+
msal_params = {
|
|
214
|
+
"authority": authority_url,
|
|
215
|
+
"client_id": self.client_id,
|
|
216
|
+
"client_credential": self.client_secret,
|
|
217
|
+
"token_cache": self._token_cache,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if self.cloud in ("government", "us"):
|
|
221
|
+
msal_params["validate_authority"] = False
|
|
222
|
+
logger.info("Disabled authority validation for government cloud")
|
|
223
|
+
|
|
224
|
+
self._msal_app = msal.ConfidentialClientApplication(**msal_params)
|
|
225
|
+
self._authority_url = authority_url
|
|
226
|
+
|
|
227
|
+
now = int(time.time())
|
|
228
|
+
if self._access_token and now < (self._access_token_exp - 60):
|
|
229
|
+
return self._access_token
|
|
230
|
+
|
|
231
|
+
last_err = None
|
|
232
|
+
for attempt in range(1, 6): # 5 attempts
|
|
233
|
+
try:
|
|
234
|
+
result = self._msal_app.acquire_token_for_client(scopes=self._scopes)
|
|
235
|
+
|
|
236
|
+
if "access_token" not in result:
|
|
237
|
+
error_desc = result.get("error_description", "Unknown error")
|
|
238
|
+
error = result.get("error", "Unknown")
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"Failed to acquire token: {error} - {error_desc}\n"
|
|
241
|
+
f"Authority: {self._authority_url}\n"
|
|
242
|
+
f"Scopes: {self._scopes}"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
token = result["access_token"]
|
|
246
|
+
|
|
247
|
+
# MSAL returns expires_in (seconds) for client credential tokens
|
|
248
|
+
expires_in = int(result.get("expires_in", 3600))
|
|
249
|
+
self._access_token = token
|
|
250
|
+
self._access_token_exp = int(time.time()) + expires_in
|
|
251
|
+
|
|
252
|
+
logger.info(f"Successfully acquired Graph API token")
|
|
253
|
+
return token
|
|
254
|
+
|
|
255
|
+
except Exception as e:
|
|
256
|
+
last_err = e
|
|
257
|
+
# Exponential backoff with jitter
|
|
258
|
+
sleep_s = min(8.0, (2 ** (attempt - 1)) * 0.5) + random.random() * 0.25
|
|
259
|
+
logger.warning(
|
|
260
|
+
f"Token acquisition attempt {attempt}/5 failed: {e}. Retrying in {sleep_s:.2f}s"
|
|
261
|
+
)
|
|
262
|
+
time.sleep(sleep_s)
|
|
263
|
+
|
|
264
|
+
# If we get here, all retries failed
|
|
265
|
+
raise RuntimeError(f"Token acquisition failed after retries: {last_err}")
|
|
266
|
+
|
|
267
|
+
|
|
195
268
|
def get_context_with_certificate(self) -> ClientContext:
|
|
196
269
|
"""
|
|
197
270
|
Get ClientContext using certificate-based authentication.
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Microsoft Graph API implementation for SharePoint operations.
|
|
3
|
+
Primary API for all SharePoint operations in Azure Government Cloud.
|
|
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
|
+
Primary client for all SharePoint operations, especially in Azure Government Cloud
|
|
19
|
+
where SharePoint REST API may not support app-only authentication.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, site_url: str, token_callback):
|
|
23
|
+
"""
|
|
24
|
+
Initialize Graph API client.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
site_url: SharePoint site URL (e.g., https://tenant.sharepoint.us/sites/SiteName)
|
|
28
|
+
token_callback: Function that returns access token
|
|
29
|
+
"""
|
|
30
|
+
self.site_url = site_url.rstrip("/")
|
|
31
|
+
self.token_callback = token_callback
|
|
32
|
+
self._site_id = None
|
|
33
|
+
self._drive_id = None # Cache drive ID to avoid repeated API calls
|
|
34
|
+
|
|
35
|
+
# Determine Graph API endpoint based on cloud
|
|
36
|
+
if ".sharepoint.us" in site_url:
|
|
37
|
+
self.graph_endpoint = "https://graph.microsoft.us/v1.0"
|
|
38
|
+
logger.info("Using Microsoft Graph US Government endpoint")
|
|
39
|
+
else:
|
|
40
|
+
self.graph_endpoint = "https://graph.microsoft.com/v1.0"
|
|
41
|
+
logger.info("Using Microsoft Graph Commercial endpoint")
|
|
42
|
+
|
|
43
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
44
|
+
"""Get authorization headers with access token."""
|
|
45
|
+
token_obj = self.token_callback()
|
|
46
|
+
# Handle both TokenResponse objects and plain strings
|
|
47
|
+
if hasattr(token_obj, 'accessToken'):
|
|
48
|
+
token = token_obj.accessToken
|
|
49
|
+
else:
|
|
50
|
+
token = str(token_obj)
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
"Authorization": f"Bearer {token}",
|
|
54
|
+
"Accept": "application/json",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def _handle_response(self, response: requests.Response) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Handle Graph API response and raise detailed errors if needed.
|
|
60
|
+
|
|
61
|
+
Graph API returns errors in format:
|
|
62
|
+
{
|
|
63
|
+
"error": {
|
|
64
|
+
"code": "itemNotFound",
|
|
65
|
+
"message": "The resource could not be found."
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
"""
|
|
69
|
+
if response.ok:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
error_data = response.json()
|
|
74
|
+
if "error" in error_data:
|
|
75
|
+
error = error_data["error"]
|
|
76
|
+
code = error.get("code", "Unknown")
|
|
77
|
+
message = error.get("message", "Unknown error")
|
|
78
|
+
raise requests.HTTPError(
|
|
79
|
+
f"Graph API error [{code}]: {message}",
|
|
80
|
+
response=response
|
|
81
|
+
)
|
|
82
|
+
except (ValueError, KeyError):
|
|
83
|
+
# If we can't parse the error, fall back to standard handling
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
self._handle_response(response)
|
|
87
|
+
|
|
88
|
+
def _get_site_id(self) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Get the site ID from the site URL.
|
|
91
|
+
Caches the result for reuse.
|
|
92
|
+
"""
|
|
93
|
+
if self._site_id:
|
|
94
|
+
return self._site_id
|
|
95
|
+
|
|
96
|
+
parsed = urlparse(self.site_url)
|
|
97
|
+
hostname = parsed.netloc
|
|
98
|
+
path = parsed.path.strip("/")
|
|
99
|
+
|
|
100
|
+
# For root site: https://tenant.sharepoint.us
|
|
101
|
+
if not path or path == "sites":
|
|
102
|
+
url = f"{self.graph_endpoint}/sites/{hostname}"
|
|
103
|
+
# For subsite: https://tenant.sharepoint.us/sites/SiteName
|
|
104
|
+
else:
|
|
105
|
+
url = f"{self.graph_endpoint}/sites/{hostname}:/{path}"
|
|
106
|
+
|
|
107
|
+
response = requests.get(url, headers=self._get_headers())
|
|
108
|
+
self._handle_response(response)
|
|
109
|
+
|
|
110
|
+
self._site_id = response.json()["id"]
|
|
111
|
+
logger.info(f"Retrieved site ID: {self._site_id}")
|
|
112
|
+
return self._site_id
|
|
113
|
+
|
|
114
|
+
def _get_drive_id(self) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Get the default document library drive ID.
|
|
117
|
+
Caches the result for reuse.
|
|
118
|
+
"""
|
|
119
|
+
if self._drive_id:
|
|
120
|
+
return self._drive_id
|
|
121
|
+
|
|
122
|
+
site_id = self._get_site_id()
|
|
123
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drive"
|
|
124
|
+
|
|
125
|
+
response = requests.get(url, headers=self._get_headers())
|
|
126
|
+
self._handle_response(response)
|
|
127
|
+
|
|
128
|
+
self._drive_id = response.json()["id"]
|
|
129
|
+
logger.info(f"Retrieved drive ID: {self._drive_id}")
|
|
130
|
+
return self._drive_id
|
|
131
|
+
|
|
132
|
+
def list_folders(self, folder_path: str = "") -> List[Dict[str, Any]]:
|
|
133
|
+
"""
|
|
134
|
+
List folders in the specified path.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
folder_path: Relative path from document library root
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
List of folder objects with name, id, webUrl
|
|
141
|
+
"""
|
|
142
|
+
site_id = self._get_site_id()
|
|
143
|
+
drive_id = self._get_drive_id()
|
|
144
|
+
|
|
145
|
+
if folder_path:
|
|
146
|
+
# URL encode the path
|
|
147
|
+
encoded_path = quote(folder_path)
|
|
148
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root:/{encoded_path}:/children"
|
|
149
|
+
else:
|
|
150
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root/children"
|
|
151
|
+
|
|
152
|
+
response = requests.get(url, headers=self._get_headers())
|
|
153
|
+
self._handle_response(response)
|
|
154
|
+
|
|
155
|
+
items = response.json().get("value", [])
|
|
156
|
+
# Filter to only folders
|
|
157
|
+
folders = [
|
|
158
|
+
{
|
|
159
|
+
"name": item["name"],
|
|
160
|
+
"id": item["id"],
|
|
161
|
+
"webUrl": item.get("webUrl", ""),
|
|
162
|
+
}
|
|
163
|
+
for item in items
|
|
164
|
+
if "folder" in item
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
logger.info(f"Found {len(folders)} folders in '{folder_path}'")
|
|
168
|
+
return folders
|
|
169
|
+
|
|
170
|
+
def list_documents(self, folder_path: str = "") -> List[Dict[str, Any]]:
|
|
171
|
+
"""
|
|
172
|
+
List documents in the specified folder.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
folder_path: Relative path from document library root
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
List of file objects with name, id, size, webUrl
|
|
179
|
+
"""
|
|
180
|
+
site_id = self._get_site_id()
|
|
181
|
+
drive_id = self._get_drive_id()
|
|
182
|
+
|
|
183
|
+
if folder_path:
|
|
184
|
+
encoded_path = quote(folder_path)
|
|
185
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root:/{encoded_path}:/children"
|
|
186
|
+
else:
|
|
187
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root/children"
|
|
188
|
+
|
|
189
|
+
response = requests.get(url, headers=self._get_headers())
|
|
190
|
+
self._handle_response(response)
|
|
191
|
+
|
|
192
|
+
items = response.json().get("value", [])
|
|
193
|
+
# Filter to only files
|
|
194
|
+
files = [
|
|
195
|
+
{
|
|
196
|
+
"name": item["name"],
|
|
197
|
+
"id": item["id"],
|
|
198
|
+
"size": item.get("size", 0),
|
|
199
|
+
"webUrl": item.get("webUrl", ""),
|
|
200
|
+
}
|
|
201
|
+
for item in items
|
|
202
|
+
if "file" in item
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
logger.info(f"Found {len(files)} files in '{folder_path}'")
|
|
206
|
+
return files
|
|
207
|
+
|
|
208
|
+
def get_file_content(self, file_path: str) -> bytes:
|
|
209
|
+
"""
|
|
210
|
+
Get the content of a file.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
file_path: Relative path to the file
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
File content as bytes
|
|
217
|
+
"""
|
|
218
|
+
site_id = self._get_site_id()
|
|
219
|
+
drive_id = self._get_drive_id()
|
|
220
|
+
|
|
221
|
+
encoded_path = quote(file_path)
|
|
222
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root:/{encoded_path}:/content"
|
|
223
|
+
|
|
224
|
+
response = requests.get(url, headers=self._get_headers())
|
|
225
|
+
self._handle_response(response)
|
|
226
|
+
|
|
227
|
+
logger.info(f"Retrieved content for '{file_path}' ({len(response.content)} bytes)")
|
|
228
|
+
return response.content
|
|
229
|
+
|
|
230
|
+
def upload_file(self, folder_path: str, file_name: str, content: bytes) -> Dict[str, Any]:
|
|
231
|
+
"""
|
|
232
|
+
Upload a file to SharePoint.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
folder_path: Destination folder path
|
|
236
|
+
file_name: Name of the file
|
|
237
|
+
content: File content as bytes
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
File metadata
|
|
241
|
+
"""
|
|
242
|
+
site_id = self._get_site_id()
|
|
243
|
+
drive_id = self._get_drive_id()
|
|
244
|
+
|
|
245
|
+
if folder_path:
|
|
246
|
+
full_path = f"{folder_path}/{file_name}"
|
|
247
|
+
else:
|
|
248
|
+
full_path = file_name
|
|
249
|
+
|
|
250
|
+
encoded_path = quote(full_path)
|
|
251
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root:/{encoded_path}:/content"
|
|
252
|
+
|
|
253
|
+
headers = self._get_headers()
|
|
254
|
+
headers["Content-Type"] = "application/octet-stream"
|
|
255
|
+
|
|
256
|
+
response = requests.put(url, headers=headers, data=content)
|
|
257
|
+
self._handle_response(response)
|
|
258
|
+
|
|
259
|
+
logger.info(f"Uploaded '{file_name}' to '{folder_path}'")
|
|
260
|
+
return response.json()
|
|
261
|
+
|
|
262
|
+
def delete_file(self, file_path: str) -> None:
|
|
263
|
+
"""
|
|
264
|
+
Delete a file.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
file_path: Relative path to the file
|
|
268
|
+
"""
|
|
269
|
+
site_id = self._get_site_id()
|
|
270
|
+
drive_id = self._get_drive_id()
|
|
271
|
+
|
|
272
|
+
encoded_path = quote(file_path)
|
|
273
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root:/{encoded_path}"
|
|
274
|
+
|
|
275
|
+
response = requests.delete(url, headers=self._get_headers())
|
|
276
|
+
self._handle_response(response)
|
|
277
|
+
|
|
278
|
+
logger.info(f"Deleted '{file_path}'")
|
|
279
|
+
|
|
280
|
+
def create_folder(self, parent_path: str, folder_name: str) -> Dict[str, Any]:
|
|
281
|
+
"""
|
|
282
|
+
Create a new folder.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
parent_path: Path to parent folder
|
|
286
|
+
folder_name: Name of the new folder
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Folder metadata
|
|
290
|
+
"""
|
|
291
|
+
site_id = self._get_site_id()
|
|
292
|
+
drive_id = self._get_drive_id()
|
|
293
|
+
|
|
294
|
+
if parent_path:
|
|
295
|
+
encoded_path = quote(parent_path)
|
|
296
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root:/{encoded_path}:/children"
|
|
297
|
+
else:
|
|
298
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root/children"
|
|
299
|
+
|
|
300
|
+
payload = {
|
|
301
|
+
"name": folder_name,
|
|
302
|
+
"folder": {},
|
|
303
|
+
"@microsoft.graph.conflictBehavior": "fail"
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
response = requests.post(url, headers=self._get_headers(), json=payload)
|
|
307
|
+
self._handle_response(response)
|
|
308
|
+
|
|
309
|
+
logger.info(f"Created folder '{folder_name}' in '{parent_path}'")
|
|
310
|
+
return response.json()
|
|
311
|
+
|
|
312
|
+
def delete_folder(self, folder_path: str) -> None:
|
|
313
|
+
"""
|
|
314
|
+
Delete a folder.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
folder_path: Relative path to the folder
|
|
318
|
+
"""
|
|
319
|
+
site_id = self._get_site_id()
|
|
320
|
+
drive_id = self._get_drive_id()
|
|
321
|
+
|
|
322
|
+
encoded_path = quote(folder_path)
|
|
323
|
+
url = f"{self.graph_endpoint}/sites/{site_id}/drives/{drive_id}/root:/{encoded_path}"
|
|
324
|
+
|
|
325
|
+
response = requests.delete(url, headers=self._get_headers())
|
|
326
|
+
self._handle_response(response)
|
|
327
|
+
|
|
328
|
+
logger.info(f"Deleted folder '{folder_path}'")
|
{mcp_sharepoint_us-2.0.11 → mcp_sharepoint_us-2.0.13}/src/mcp_sharepoint_us.egg-info/PKG-INFO
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-sharepoint-us
|
|
3
|
-
Version: 2.0.
|
|
4
|
-
Summary: SharePoint MCP Server with
|
|
3
|
+
Version: 2.0.13
|
|
4
|
+
Summary: SharePoint MCP Server with Microsoft Graph API
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/mdev26/mcp-sharepoint-us
|
|
7
7
|
Project-URL: Repository, 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"
|
{mcp_sharepoint_us-2.0.11 → mcp_sharepoint_us-2.0.13}/src/mcp_sharepoint_us.egg-info/SOURCES.txt
RENAMED
|
@@ -4,6 +4,7 @@ pyproject.toml
|
|
|
4
4
|
src/mcp_sharepoint/__init__.py
|
|
5
5
|
src/mcp_sharepoint/__main__.py
|
|
6
6
|
src/mcp_sharepoint/auth.py
|
|
7
|
+
src/mcp_sharepoint/graph_api.py
|
|
7
8
|
src/mcp_sharepoint_us.egg-info/PKG-INFO
|
|
8
9
|
src/mcp_sharepoint_us.egg-info/SOURCES.txt
|
|
9
10
|
src/mcp_sharepoint_us.egg-info/dependency_links.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_sharepoint_us-2.0.11 → mcp_sharepoint_us-2.0.13}/src/mcp_sharepoint_us.egg-info/top_level.txt
RENAMED
|
File without changes
|