mcp-sharepoint-us 2.0.4__py3-none-any.whl → 2.0.5__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.
- mcp_sharepoint/__init__.py +24 -18
- mcp_sharepoint/auth.py +73 -33
- {mcp_sharepoint_us-2.0.4.dist-info → mcp_sharepoint_us-2.0.5.dist-info}/METADATA +1 -1
- mcp_sharepoint_us-2.0.5.dist-info/RECORD +9 -0
- mcp_sharepoint_us-2.0.4.dist-info/RECORD +0 -9
- {mcp_sharepoint_us-2.0.4.dist-info → mcp_sharepoint_us-2.0.5.dist-info}/WHEEL +0 -0
- {mcp_sharepoint_us-2.0.4.dist-info → mcp_sharepoint_us-2.0.5.dist-info}/entry_points.txt +0 -0
- {mcp_sharepoint_us-2.0.4.dist-info → mcp_sharepoint_us-2.0.5.dist-info}/licenses/LICENSE +0 -0
- {mcp_sharepoint_us-2.0.4.dist-info → mcp_sharepoint_us-2.0.5.dist-info}/top_level.txt +0 -0
mcp_sharepoint/__init__.py
CHANGED
|
@@ -288,13 +288,14 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
|
288
288
|
raise ValueError(f"Unknown tool: {name}")
|
|
289
289
|
|
|
290
290
|
except Exception as e:
|
|
291
|
-
logger.
|
|
291
|
+
logger.exception(f"Tool '{name}' failed") # <-- prints stack trace
|
|
292
292
|
return [TextContent(
|
|
293
293
|
type="text",
|
|
294
294
|
text=f"Error executing {name}: {str(e)}"
|
|
295
295
|
)]
|
|
296
296
|
|
|
297
297
|
|
|
298
|
+
|
|
298
299
|
async def test_connection() -> list[TextContent]:
|
|
299
300
|
"""Test SharePoint connection"""
|
|
300
301
|
try:
|
|
@@ -369,28 +370,33 @@ async def get_document_content(file_path: str) -> list[TextContent]:
|
|
|
369
370
|
try:
|
|
370
371
|
doc_lib = get_document_library_path()
|
|
371
372
|
full_path = f"{doc_lib}/{file_path}"
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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)
|
|
380
|
+
|
|
377
381
|
ext = os.path.splitext(file_path)[1].lower()
|
|
378
382
|
text_extensions = {'.txt', '.md', '.json', '.xml', '.html', '.csv', '.log'}
|
|
379
|
-
|
|
383
|
+
|
|
380
384
|
if ext in text_extensions:
|
|
381
|
-
|
|
382
|
-
text_content = content.decode('utf-8')
|
|
385
|
+
text_content = content.decode("utf-8", errors="replace")
|
|
383
386
|
return [TextContent(type="text", text=text_content)]
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
387
|
+
|
|
388
|
+
b64_content = base64.b64encode(content).decode("utf-8")
|
|
389
|
+
return [TextContent(
|
|
390
|
+
type="text",
|
|
391
|
+
text=(
|
|
392
|
+
"Binary file (base64 encoded):\n\n"
|
|
393
|
+
f"{b64_content[:200]}...\n\n"
|
|
394
|
+
f"Full content length: {len(b64_content)} characters"
|
|
395
|
+
)
|
|
396
|
+
)]
|
|
397
|
+
|
|
393
398
|
except Exception as e:
|
|
399
|
+
logger.exception("Error reading document")
|
|
394
400
|
return [TextContent(type="text", text=f"Error reading document: {str(e)}")]
|
|
395
401
|
|
|
396
402
|
|
mcp_sharepoint/auth.py
CHANGED
|
@@ -4,6 +4,8 @@ Supports Azure US Government Cloud and Commercial Cloud
|
|
|
4
4
|
"""
|
|
5
5
|
import os
|
|
6
6
|
import logging
|
|
7
|
+
import time
|
|
8
|
+
import random
|
|
7
9
|
from typing import Optional
|
|
8
10
|
from office365.sharepoint.client_context import ClientContext
|
|
9
11
|
from office365.runtime.auth.client_credential import ClientCredential
|
|
@@ -51,49 +53,87 @@ class SharePointAuthenticator:
|
|
|
51
53
|
def get_context_with_msal(self) -> ClientContext:
|
|
52
54
|
"""
|
|
53
55
|
Get ClientContext using MSAL for modern Azure AD authentication.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
Returns:
|
|
57
|
-
Authenticated ClientContext
|
|
56
|
+
Uses a cached MSAL app + simple in-memory token cache to avoid repeated
|
|
57
|
+
OIDC discovery calls and reduce connection resets.
|
|
58
58
|
"""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if self.cloud
|
|
63
|
-
|
|
64
|
-
authority_url = f'https://login.microsoftonline.us/{self.tenant_id}'
|
|
59
|
+
|
|
60
|
+
# Build and cache the MSAL app once per authenticator instance
|
|
61
|
+
if not hasattr(self, "_msal_app"):
|
|
62
|
+
if self.cloud in ("government", "us"):
|
|
63
|
+
authority_url = f"https://login.microsoftonline.us/{self.tenant_id}"
|
|
65
64
|
logger.info("Using Azure US Government Cloud endpoints")
|
|
66
65
|
else:
|
|
67
|
-
|
|
68
|
-
authority_url = f'https://login.microsoftonline.com/{self.tenant_id}'
|
|
66
|
+
authority_url = f"https://login.microsoftonline.com/{self.tenant_id}"
|
|
69
67
|
logger.info("Using Azure Commercial Cloud endpoints")
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
|
|
69
|
+
# Optional: enable MSAL token cache (in-memory). Helps reduce calls.
|
|
70
|
+
self._token_cache = getattr(self, "_token_cache", msal.SerializableTokenCache())
|
|
71
|
+
|
|
72
|
+
self._msal_app = msal.ConfidentialClientApplication(
|
|
72
73
|
authority=authority_url,
|
|
73
74
|
client_id=self.client_id,
|
|
74
|
-
client_credential=self.client_secret
|
|
75
|
+
client_credential=self.client_secret,
|
|
76
|
+
token_cache=self._token_cache,
|
|
75
77
|
)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
78
|
+
self._authority_url = authority_url
|
|
79
|
+
|
|
80
|
+
# Small in-memory access-token cache (avoid repeated acquire calls)
|
|
81
|
+
# MSAL caches too, but keeping the raw token avoids extra work in Office365 callbacks.
|
|
82
|
+
if not hasattr(self, "_access_token"):
|
|
83
|
+
self._access_token = None
|
|
84
|
+
self._access_token_exp = 0
|
|
85
|
+
|
|
86
|
+
scopes = [f"{self.site_url}/.default"]
|
|
87
|
+
|
|
88
|
+
def acquire_token() -> str:
|
|
89
|
+
"""
|
|
90
|
+
Token callback used by office365 ClientContext.
|
|
91
|
+
Retries transient network errors like ConnectionResetError(104).
|
|
92
|
+
"""
|
|
93
|
+
now = int(time.time())
|
|
94
|
+
if self._access_token and now < (self._access_token_exp - 60):
|
|
95
|
+
return self._access_token
|
|
96
|
+
|
|
97
|
+
last_err = None
|
|
98
|
+
for attempt in range(1, 6): # 5 attempts
|
|
99
|
+
try:
|
|
100
|
+
result = self._msal_app.acquire_token_for_client(scopes=scopes)
|
|
101
|
+
|
|
102
|
+
if "access_token" not in result:
|
|
103
|
+
error_desc = result.get("error_description", "Unknown error")
|
|
104
|
+
error = result.get("error", "Unknown")
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"Failed to acquire token: {error} - {error_desc}\n"
|
|
107
|
+
f"Authority: {self._authority_url}\n"
|
|
108
|
+
f"Scopes: {scopes}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
token = result["access_token"]
|
|
112
|
+
|
|
113
|
+
# MSAL returns expires_in (seconds) for client credential tokens
|
|
114
|
+
expires_in = int(result.get("expires_in", 3600))
|
|
115
|
+
self._access_token = token
|
|
116
|
+
self._access_token_exp = int(time.time()) + expires_in
|
|
117
|
+
|
|
118
|
+
logger.info(f"Successfully acquired token for {self.site_url}")
|
|
119
|
+
return token
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
last_err = e
|
|
123
|
+
# Exponential backoff with jitter
|
|
124
|
+
sleep_s = min(8.0, (2 ** (attempt - 1)) * 0.5) + random.random() * 0.25
|
|
125
|
+
logger.warning(
|
|
126
|
+
f"Token acquisition attempt {attempt}/5 failed: {e}. Retrying in {sleep_s:.2f}s"
|
|
127
|
+
)
|
|
128
|
+
time.sleep(sleep_s)
|
|
129
|
+
|
|
130
|
+
# If we get here, all retries failed
|
|
131
|
+
raise RuntimeError(f"Token acquisition failed after retries: {last_err}")
|
|
132
|
+
|
|
94
133
|
ctx = ClientContext(self.site_url).with_access_token(acquire_token)
|
|
95
134
|
logger.info("Successfully authenticated using MSAL (Modern Azure AD)")
|
|
96
135
|
return ctx
|
|
136
|
+
|
|
97
137
|
|
|
98
138
|
def get_context_with_certificate(self) -> ClientContext:
|
|
99
139
|
"""
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
mcp_sharepoint/__init__.py,sha256=vnnELXIj7FdcW8KIYoeXlwq4FblG63RTrXGGSO8KdSU,19412
|
|
2
|
+
mcp_sharepoint/__main__.py,sha256=4iVDdDZx4rQ4Zo-x0RaCrT-NKeGObIz_ks3YF8di2nA,132
|
|
3
|
+
mcp_sharepoint/auth.py,sha256=J_NV2XN-4qv8d-i-P6_btYDdExEWi4BSw0jO-2kCqlk,11984
|
|
4
|
+
mcp_sharepoint_us-2.0.5.dist-info/licenses/LICENSE,sha256=SRM8juGH4GjIqnl5rrp-P-S5mW5h2mINOPx5-wOZG6s,1112
|
|
5
|
+
mcp_sharepoint_us-2.0.5.dist-info/METADATA,sha256=-XkuUvhJAC2npnvlrv1PJzV9SOmlPPDiDTwNE6QB1qA,11379
|
|
6
|
+
mcp_sharepoint_us-2.0.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
7
|
+
mcp_sharepoint_us-2.0.5.dist-info/entry_points.txt,sha256=UZOa_7OLI41rmsErbvnSz9RahPMGQVcqZUFMphOcjbY,57
|
|
8
|
+
mcp_sharepoint_us-2.0.5.dist-info/top_level.txt,sha256=R6mRoWe61lz4kUSKGV6S2XVbE7825xfC_J-ouZIYpuo,15
|
|
9
|
+
mcp_sharepoint_us-2.0.5.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
mcp_sharepoint/__init__.py,sha256=rm3OXtkIFtpDjVfoBPZQRy3N3w7vptvaRPEnEnDQGiY,19305
|
|
2
|
-
mcp_sharepoint/__main__.py,sha256=4iVDdDZx4rQ4Zo-x0RaCrT-NKeGObIz_ks3YF8di2nA,132
|
|
3
|
-
mcp_sharepoint/auth.py,sha256=_nrx4GZcnNyVEQqiqvK6Y6DDnXeiZtEwyWL1WlDjnTM,10164
|
|
4
|
-
mcp_sharepoint_us-2.0.4.dist-info/licenses/LICENSE,sha256=SRM8juGH4GjIqnl5rrp-P-S5mW5h2mINOPx5-wOZG6s,1112
|
|
5
|
-
mcp_sharepoint_us-2.0.4.dist-info/METADATA,sha256=qEBmFAmjTfmy_cn5EOqkNXexv6kk17lrfRt6t2fIy2o,11379
|
|
6
|
-
mcp_sharepoint_us-2.0.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
7
|
-
mcp_sharepoint_us-2.0.4.dist-info/entry_points.txt,sha256=UZOa_7OLI41rmsErbvnSz9RahPMGQVcqZUFMphOcjbY,57
|
|
8
|
-
mcp_sharepoint_us-2.0.4.dist-info/top_level.txt,sha256=R6mRoWe61lz4kUSKGV6S2XVbE7825xfC_J-ouZIYpuo,15
|
|
9
|
-
mcp_sharepoint_us-2.0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|