mcp-sharepoint-us 2.0.3__tar.gz → 2.0.5__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-sharepoint-us
3
- Version: 2.0.3
3
+ Version: 2.0.5
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mcp-sharepoint-us"
7
- version = "2.0.3"
7
+ version = "2.0.5"
8
8
  description = "SharePoint MCP Server with Modern Azure AD Authentication"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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.error(f"Tool '{name}' failed: {e}")
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
- file = ctx.web.get_file_by_server_relative_path(full_path)
374
- content = file.read()
375
-
376
- # Determine if binary based on file extension
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
- # Text file
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
- else:
385
- # Binary file - return base64
386
- b64_content = base64.b64encode(content).decode('utf-8')
387
- return [TextContent(
388
- type="text",
389
- text=f"Binary file (base64 encoded):\n\n{b64_content[:200]}...\n\n"
390
- f"Full content length: {len(b64_content)} characters"
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
 
@@ -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
- Supports both Commercial and US Government clouds.
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
- def acquire_token():
60
- """Acquire token using MSAL"""
61
- # Set authority URL based on cloud environment
62
- if self.cloud == "government" or self.cloud == "us":
63
- # Azure US Government Cloud
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
- # Commercial Cloud
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
- app = msal.ConfidentialClientApplication(
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
- # SharePoint requires the site-specific scope
78
- scopes = [f"{self.site_url}/.default"]
79
-
80
- result = app.acquire_token_for_client(scopes=scopes)
81
-
82
- if "access_token" not in result:
83
- error_desc = result.get("error_description", "Unknown error")
84
- error = result.get("error", "Unknown")
85
- raise ValueError(
86
- f"Failed to acquire token: {error} - {error_desc}\n"
87
- f"Authority: {authority_url}\n"
88
- f"Scopes: {scopes}"
89
- )
90
-
91
- logger.info(f"Successfully acquired token for {self.site_url}")
92
- return result
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
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-sharepoint-us
3
- Version: 2.0.3
3
+ Version: 2.0.5
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