mcp-sharepoint-us 2.0.0__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.

@@ -0,0 +1,535 @@
1
+ """
2
+ SharePoint MCP Server with Modern Azure AD Authentication
3
+ """
4
+ import os
5
+ import logging
6
+ import asyncio
7
+ from functools import wraps
8
+ from typing import Optional
9
+ import base64
10
+ import mimetypes
11
+
12
+ from mcp.server import Server
13
+ from mcp.types import Resource, Tool, TextContent, ImageContent, EmbeddedResource
14
+ from pydantic import AnyUrl
15
+ import mcp.server.stdio
16
+
17
+ from office365.sharepoint.files.file import File
18
+ from office365.sharepoint.folders.folder import Folder
19
+ from office365.sharepoint.client_context import ClientContext
20
+
21
+ from .auth import create_sharepoint_context
22
+
23
+ # Setup logging
24
+ logging.basicConfig(level=logging.INFO)
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # Initialize MCP server
28
+ app = Server("mcp-sharepoint")
29
+
30
+ # Global SharePoint context
31
+ ctx: Optional[ClientContext] = None
32
+
33
+
34
+ def ensure_context(func):
35
+ """Decorator to ensure SharePoint context is available"""
36
+ @wraps(func)
37
+ async def wrapper(*args, **kwargs):
38
+ global ctx
39
+ if ctx is None:
40
+ try:
41
+ ctx = create_sharepoint_context()
42
+ logger.info("SharePoint context initialized successfully")
43
+ except Exception as e:
44
+ logger.error(f"Failed to initialize SharePoint context: {e}")
45
+ raise RuntimeError(
46
+ f"SharePoint authentication failed: {e}. "
47
+ "Please check your environment variables and ensure:\n"
48
+ "1. SHP_TENANT_ID is set correctly\n"
49
+ "2. Your Azure AD app has the correct API permissions\n"
50
+ "3. If using a new tenant, make sure you're using modern auth (MSAL)"
51
+ )
52
+ return await func(*args, **kwargs)
53
+ return wrapper
54
+
55
+
56
+ def get_document_library_path() -> str:
57
+ """Get the document library path from environment"""
58
+ return os.getenv("SHP_DOC_LIBRARY", "Shared Documents")
59
+
60
+
61
+ @app.list_resources()
62
+ async def list_resources() -> list[Resource]:
63
+ """List available SharePoint resources"""
64
+ return [
65
+ Resource(
66
+ uri=AnyUrl(f"sharepoint:///{get_document_library_path()}"),
67
+ name=f"SharePoint Document Library: {get_document_library_path()}",
68
+ mimeType="application/vnd.sharepoint.folder",
69
+ description="Main SharePoint document library configured for this server"
70
+ )
71
+ ]
72
+
73
+
74
+ @app.list_tools()
75
+ async def list_tools() -> list[Tool]:
76
+ """List available SharePoint tools"""
77
+ return [
78
+ Tool(
79
+ name="List_SharePoint_Folders",
80
+ description="List all folders in a specified directory or root of the document library",
81
+ inputSchema={
82
+ "type": "object",
83
+ "properties": {
84
+ "folder_path": {
85
+ "type": "string",
86
+ "description": "Path to the folder (relative to document library root). Leave empty for root.",
87
+ "default": ""
88
+ }
89
+ }
90
+ }
91
+ ),
92
+ Tool(
93
+ name="List_SharePoint_Documents",
94
+ description="List all documents in a specified folder with metadata",
95
+ inputSchema={
96
+ "type": "object",
97
+ "properties": {
98
+ "folder_path": {
99
+ "type": "string",
100
+ "description": "Path to the folder containing documents",
101
+ "default": ""
102
+ }
103
+ },
104
+ "required": []
105
+ }
106
+ ),
107
+ Tool(
108
+ name="Get_Document_Content",
109
+ description="Get the content of a document (supports text extraction from PDF, Word, Excel, and text files)",
110
+ inputSchema={
111
+ "type": "object",
112
+ "properties": {
113
+ "file_path": {
114
+ "type": "string",
115
+ "description": "Path to the file (relative to document library root)"
116
+ }
117
+ },
118
+ "required": ["file_path"]
119
+ }
120
+ ),
121
+ Tool(
122
+ name="Upload_Document",
123
+ description="Upload a new document to SharePoint",
124
+ inputSchema={
125
+ "type": "object",
126
+ "properties": {
127
+ "folder_path": {
128
+ "type": "string",
129
+ "description": "Destination folder path"
130
+ },
131
+ "file_name": {
132
+ "type": "string",
133
+ "description": "Name of the file to create"
134
+ },
135
+ "content": {
136
+ "type": "string",
137
+ "description": "File content (text or base64 encoded for binary files)"
138
+ },
139
+ "is_binary": {
140
+ "type": "boolean",
141
+ "description": "Whether the content is base64 encoded binary",
142
+ "default": False
143
+ }
144
+ },
145
+ "required": ["folder_path", "file_name", "content"]
146
+ }
147
+ ),
148
+ Tool(
149
+ name="Update_Document",
150
+ description="Update the content of an existing document",
151
+ inputSchema={
152
+ "type": "object",
153
+ "properties": {
154
+ "file_path": {
155
+ "type": "string",
156
+ "description": "Path to the file to update"
157
+ },
158
+ "content": {
159
+ "type": "string",
160
+ "description": "New file content"
161
+ },
162
+ "is_binary": {
163
+ "type": "boolean",
164
+ "description": "Whether the content is base64 encoded binary",
165
+ "default": False
166
+ }
167
+ },
168
+ "required": ["file_path", "content"]
169
+ }
170
+ ),
171
+ Tool(
172
+ name="Delete_Document",
173
+ description="Delete a document from SharePoint",
174
+ inputSchema={
175
+ "type": "object",
176
+ "properties": {
177
+ "file_path": {
178
+ "type": "string",
179
+ "description": "Path to the file to delete"
180
+ }
181
+ },
182
+ "required": ["file_path"]
183
+ }
184
+ ),
185
+ Tool(
186
+ name="Create_Folder",
187
+ description="Create a new folder in SharePoint",
188
+ inputSchema={
189
+ "type": "object",
190
+ "properties": {
191
+ "folder_path": {
192
+ "type": "string",
193
+ "description": "Path where to create the folder"
194
+ },
195
+ "folder_name": {
196
+ "type": "string",
197
+ "description": "Name of the new folder"
198
+ }
199
+ },
200
+ "required": ["folder_path", "folder_name"]
201
+ }
202
+ ),
203
+ Tool(
204
+ name="Delete_Folder",
205
+ description="Delete an empty folder from SharePoint",
206
+ inputSchema={
207
+ "type": "object",
208
+ "properties": {
209
+ "folder_path": {
210
+ "type": "string",
211
+ "description": "Path to the folder to delete"
212
+ }
213
+ },
214
+ "required": ["folder_path"]
215
+ }
216
+ ),
217
+ Tool(
218
+ name="Get_SharePoint_Tree",
219
+ description="Get a recursive tree view of SharePoint folder structure",
220
+ inputSchema={
221
+ "type": "object",
222
+ "properties": {
223
+ "folder_path": {
224
+ "type": "string",
225
+ "description": "Starting folder path (leave empty for root)",
226
+ "default": ""
227
+ },
228
+ "max_depth": {
229
+ "type": "integer",
230
+ "description": "Maximum depth to traverse",
231
+ "default": 5
232
+ }
233
+ },
234
+ "required": []
235
+ }
236
+ ),
237
+ Tool(
238
+ name="Test_Connection",
239
+ description="Test the SharePoint connection and authentication",
240
+ inputSchema={
241
+ "type": "object",
242
+ "properties": {},
243
+ "required": []
244
+ }
245
+ )
246
+ ]
247
+
248
+
249
+ @app.call_tool()
250
+ @ensure_context
251
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
252
+ """Handle tool execution"""
253
+
254
+ try:
255
+ if name == "Test_Connection":
256
+ return await test_connection()
257
+ elif name == "List_SharePoint_Folders":
258
+ return await list_folders(arguments.get("folder_path", ""))
259
+ elif name == "List_SharePoint_Documents":
260
+ return await list_documents(arguments.get("folder_path", ""))
261
+ elif name == "Get_Document_Content":
262
+ return await get_document_content(arguments["file_path"])
263
+ elif name == "Upload_Document":
264
+ return await upload_document(
265
+ arguments["folder_path"],
266
+ arguments["file_name"],
267
+ arguments["content"],
268
+ arguments.get("is_binary", False)
269
+ )
270
+ elif name == "Update_Document":
271
+ return await update_document(
272
+ arguments["file_path"],
273
+ arguments["content"],
274
+ arguments.get("is_binary", False)
275
+ )
276
+ elif name == "Delete_Document":
277
+ return await delete_document(arguments["file_path"])
278
+ elif name == "Create_Folder":
279
+ return await create_folder(arguments["folder_path"], arguments["folder_name"])
280
+ elif name == "Delete_Folder":
281
+ return await delete_folder(arguments["folder_path"])
282
+ elif name == "Get_SharePoint_Tree":
283
+ return await get_tree(
284
+ arguments.get("folder_path", ""),
285
+ arguments.get("max_depth", 5)
286
+ )
287
+ else:
288
+ raise ValueError(f"Unknown tool: {name}")
289
+
290
+ except Exception as e:
291
+ logger.error(f"Tool '{name}' failed: {e}")
292
+ return [TextContent(
293
+ type="text",
294
+ text=f"Error executing {name}: {str(e)}"
295
+ )]
296
+
297
+
298
+ async def test_connection() -> list[TextContent]:
299
+ """Test SharePoint connection"""
300
+ try:
301
+ web = ctx.web.get().execute_query()
302
+ auth_method = os.getenv("SHP_AUTH_METHOD", "msal")
303
+
304
+ return [TextContent(
305
+ type="text",
306
+ text=f"✓ Successfully connected to SharePoint!\n\n"
307
+ f"Site Title: {web.title}\n"
308
+ f"Site URL: {web.url}\n"
309
+ f"Authentication Method: {auth_method.upper()}\n"
310
+ f"Tenant ID: {os.getenv('SHP_TENANT_ID')}\n\n"
311
+ f"Connection is working correctly with modern Azure AD authentication."
312
+ )]
313
+ except Exception as e:
314
+ return [TextContent(
315
+ type="text",
316
+ text=f"✗ Connection failed: {str(e)}\n\n"
317
+ f"This usually means:\n"
318
+ f"1. Your credentials are incorrect\n"
319
+ f"2. Your app doesn't have proper SharePoint permissions\n"
320
+ f"3. You're using legacy auth on a new tenant (set SHP_AUTH_METHOD=msal)"
321
+ )]
322
+
323
+
324
+ async def list_folders(folder_path: str = "") -> list[TextContent]:
325
+ """List folders in specified path"""
326
+ try:
327
+ doc_lib = get_document_library_path()
328
+ full_path = f"{doc_lib}/{folder_path}" if folder_path else doc_lib
329
+
330
+ folder = ctx.web.get_folder_by_server_relative_path(full_path)
331
+ folders = folder.folders.get().execute_query()
332
+
333
+ folder_list = []
334
+ for f in folders:
335
+ folder_list.append(f"📁 {f.name}")
336
+
337
+ result = f"Folders in '{full_path}':\n\n" + "\n".join(folder_list) if folder_list else f"No folders found in '{full_path}'"
338
+
339
+ return [TextContent(type="text", text=result)]
340
+
341
+ except Exception as e:
342
+ return [TextContent(type="text", text=f"Error listing folders: {str(e)}")]
343
+
344
+
345
+ async def list_documents(folder_path: str = "") -> list[TextContent]:
346
+ """List documents in specified folder"""
347
+ try:
348
+ doc_lib = get_document_library_path()
349
+ full_path = f"{doc_lib}/{folder_path}" if folder_path else doc_lib
350
+
351
+ folder = ctx.web.get_folder_by_server_relative_path(full_path)
352
+ files = folder.files.get().execute_query()
353
+
354
+ file_list = []
355
+ for f in files:
356
+ size_kb = f.length / 1024
357
+ file_list.append(f"📄 {f.name} ({size_kb:.2f} KB)")
358
+
359
+ result = f"Documents in '{full_path}':\n\n" + "\n".join(file_list) if file_list else f"No documents found in '{full_path}'"
360
+
361
+ return [TextContent(type="text", text=result)]
362
+
363
+ except Exception as e:
364
+ return [TextContent(type="text", text=f"Error listing documents: {str(e)}")]
365
+
366
+
367
+ async def get_document_content(file_path: str) -> list[TextContent]:
368
+ """Get document content"""
369
+ try:
370
+ doc_lib = get_document_library_path()
371
+ 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
377
+ ext = os.path.splitext(file_path)[1].lower()
378
+ text_extensions = {'.txt', '.md', '.json', '.xml', '.html', '.csv', '.log'}
379
+
380
+ if ext in text_extensions:
381
+ # Text file
382
+ text_content = content.decode('utf-8')
383
+ 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
+
393
+ except Exception as e:
394
+ return [TextContent(type="text", text=f"Error reading document: {str(e)}")]
395
+
396
+
397
+ async def upload_document(folder_path: str, file_name: str, content: str, is_binary: bool = False) -> list[TextContent]:
398
+ """Upload a document"""
399
+ try:
400
+ doc_lib = get_document_library_path()
401
+ full_path = f"{doc_lib}/{folder_path}" if folder_path else doc_lib
402
+
403
+ folder = ctx.web.get_folder_by_server_relative_path(full_path)
404
+
405
+ if is_binary:
406
+ file_content = base64.b64decode(content)
407
+ else:
408
+ file_content = content.encode('utf-8')
409
+
410
+ uploaded_file = folder.upload_file(file_name, file_content).execute_query()
411
+
412
+ return [TextContent(
413
+ type="text",
414
+ text=f"✓ Successfully uploaded '{file_name}' to '{full_path}'"
415
+ )]
416
+
417
+ except Exception as e:
418
+ return [TextContent(type="text", text=f"Error uploading document: {str(e)}")]
419
+
420
+
421
+ async def update_document(file_path: str, content: str, is_binary: bool = False) -> list[TextContent]:
422
+ """Update a document"""
423
+ try:
424
+ doc_lib = get_document_library_path()
425
+ full_path = f"{doc_lib}/{file_path}"
426
+
427
+ if is_binary:
428
+ file_content = base64.b64decode(content)
429
+ else:
430
+ file_content = content.encode('utf-8')
431
+
432
+ file = ctx.web.get_file_by_server_relative_path(full_path)
433
+ file.write(file_content).execute_query()
434
+
435
+ return [TextContent(
436
+ type="text",
437
+ text=f"✓ Successfully updated '{file_path}'"
438
+ )]
439
+
440
+ except Exception as e:
441
+ return [TextContent(type="text", text=f"Error updating document: {str(e)}")]
442
+
443
+
444
+ async def delete_document(file_path: str) -> list[TextContent]:
445
+ """Delete a document"""
446
+ try:
447
+ doc_lib = get_document_library_path()
448
+ full_path = f"{doc_lib}/{file_path}"
449
+
450
+ file = ctx.web.get_file_by_server_relative_path(full_path)
451
+ file.delete_object().execute_query()
452
+
453
+ return [TextContent(
454
+ type="text",
455
+ text=f"✓ Successfully deleted '{file_path}'"
456
+ )]
457
+
458
+ except Exception as e:
459
+ return [TextContent(type="text", text=f"Error deleting document: {str(e)}")]
460
+
461
+
462
+ async def create_folder(folder_path: str, folder_name: str) -> list[TextContent]:
463
+ """Create a folder"""
464
+ try:
465
+ doc_lib = get_document_library_path()
466
+ full_path = f"{doc_lib}/{folder_path}" if folder_path else doc_lib
467
+
468
+ parent_folder = ctx.web.get_folder_by_server_relative_path(full_path)
469
+ new_folder = parent_folder.folders.add(folder_name).execute_query()
470
+
471
+ return [TextContent(
472
+ type="text",
473
+ text=f"✓ Successfully created folder '{folder_name}' in '{full_path}'"
474
+ )]
475
+
476
+ except Exception as e:
477
+ return [TextContent(type="text", text=f"Error creating folder: {str(e)}")]
478
+
479
+
480
+ async def delete_folder(folder_path: str) -> list[TextContent]:
481
+ """Delete a folder"""
482
+ try:
483
+ doc_lib = get_document_library_path()
484
+ full_path = f"{doc_lib}/{folder_path}"
485
+
486
+ folder = ctx.web.get_folder_by_server_relative_path(full_path)
487
+ folder.delete_object().execute_query()
488
+
489
+ return [TextContent(
490
+ type="text",
491
+ text=f"✓ Successfully deleted folder '{folder_path}'"
492
+ )]
493
+
494
+ except Exception as e:
495
+ return [TextContent(type="text", text=f"Error deleting folder: {str(e)}")]
496
+
497
+
498
+ async def get_tree(folder_path: str = "", max_depth: int = 5, current_depth: int = 0) -> list[TextContent]:
499
+ """Get folder tree structure"""
500
+ if current_depth >= max_depth:
501
+ return [TextContent(type="text", text="Max depth reached")]
502
+
503
+ try:
504
+ doc_lib = get_document_library_path()
505
+ full_path = f"{doc_lib}/{folder_path}" if folder_path else doc_lib
506
+
507
+ folder = ctx.web.get_folder_by_server_relative_path(full_path)
508
+ folders = folder.folders.get().execute_query()
509
+
510
+ indent = " " * current_depth
511
+ tree_lines = [f"{indent}📁 {folder_path or 'Root'}"]
512
+
513
+ for f in folders:
514
+ sub_path = f"{folder_path}/{f.name}" if folder_path else f.name
515
+ sub_tree = await get_tree(sub_path, max_depth, current_depth + 1)
516
+ tree_lines.append(sub_tree[0].text)
517
+
518
+ return [TextContent(type="text", text="\n".join(tree_lines))]
519
+
520
+ except Exception as e:
521
+ return [TextContent(type="text", text=f"Error getting tree: {str(e)}")]
522
+
523
+
524
+ async def main():
525
+ """Main entry point"""
526
+ async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
527
+ await app.run(
528
+ read_stream,
529
+ write_stream,
530
+ app.create_initialization_options()
531
+ )
532
+
533
+
534
+ if __name__ == "__main__":
535
+ asyncio.run(main())
@@ -0,0 +1,8 @@
1
+ """
2
+ Entry point for mcp-sharepoint module
3
+ """
4
+ import asyncio
5
+ from . import main
6
+
7
+ if __name__ == "__main__":
8
+ asyncio.run(main())
mcp_sharepoint/auth.py ADDED
@@ -0,0 +1,234 @@
1
+ """
2
+ Authentication module for SharePoint MCP Server
3
+ Supports modern Azure AD authentication methods
4
+ """
5
+ import os
6
+ import logging
7
+ from typing import Optional
8
+ from office365.sharepoint.client_context import ClientContext
9
+ from office365.runtime.auth.client_credential import ClientCredential
10
+ import msal
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class SharePointAuthenticator:
16
+ """
17
+ Handles authentication to SharePoint using modern Azure AD methods.
18
+ Supports multiple authentication flows for compatibility with new tenants.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ site_url: str,
24
+ client_id: str,
25
+ client_secret: str,
26
+ tenant_id: str,
27
+ cert_path: Optional[str] = None,
28
+ cert_thumbprint: Optional[str] = None,
29
+ ):
30
+ """
31
+ Initialize SharePoint authenticator.
32
+
33
+ Args:
34
+ site_url: SharePoint site URL
35
+ client_id: Azure AD application client ID
36
+ client_secret: Azure AD application client secret
37
+ tenant_id: Azure AD tenant ID
38
+ cert_path: Optional path to certificate file for cert-based auth
39
+ cert_thumbprint: Optional certificate thumbprint
40
+ """
41
+ self.site_url = site_url.rstrip("/")
42
+ self.client_id = client_id
43
+ self.client_secret = client_secret
44
+ self.tenant_id = tenant_id
45
+ self.cert_path = cert_path
46
+ self.cert_thumbprint = cert_thumbprint
47
+
48
+ def get_context_with_msal(self) -> ClientContext:
49
+ """
50
+ Get ClientContext using MSAL for modern Azure AD authentication.
51
+ This is the recommended method for new tenants.
52
+
53
+ Returns:
54
+ Authenticated ClientContext
55
+ """
56
+ def acquire_token():
57
+ """Acquire token using MSAL"""
58
+ authority_url = f'https://login.microsoftonline.com/{self.tenant_id}'
59
+
60
+ app = msal.ConfidentialClientApplication(
61
+ authority=authority_url,
62
+ client_id=self.client_id,
63
+ client_credential=self.client_secret
64
+ )
65
+
66
+ # SharePoint requires the site-specific scope
67
+ scopes = [f"{self.site_url}/.default"]
68
+
69
+ result = app.acquire_token_for_client(scopes=scopes)
70
+
71
+ if "access_token" not in result:
72
+ error_desc = result.get("error_description", "Unknown error")
73
+ raise ValueError(f"Failed to acquire token: {error_desc}")
74
+
75
+ return result
76
+
77
+ ctx = ClientContext(self.site_url).with_access_token(acquire_token)
78
+ logger.info("Successfully authenticated using MSAL (Modern Azure AD)")
79
+ return ctx
80
+
81
+ def get_context_with_certificate(self) -> ClientContext:
82
+ """
83
+ Get ClientContext using certificate-based authentication.
84
+ This is an alternative modern authentication method.
85
+
86
+ Returns:
87
+ Authenticated ClientContext
88
+
89
+ Raises:
90
+ ValueError: If certificate credentials are not provided
91
+ """
92
+ if not self.cert_path or not self.cert_thumbprint:
93
+ raise ValueError(
94
+ "Certificate path and thumbprint are required for cert-based auth"
95
+ )
96
+
97
+ ctx = ClientContext(self.site_url).with_client_certificate(
98
+ tenant=self.tenant_id,
99
+ client_id=self.client_id,
100
+ thumbprint=self.cert_thumbprint,
101
+ cert_path=self.cert_path
102
+ )
103
+
104
+ logger.info("Successfully authenticated using certificate")
105
+ return ctx
106
+
107
+ def get_context_legacy(self) -> ClientContext:
108
+ """
109
+ Get ClientContext using legacy ACS authentication (deprecated).
110
+ This method is included for backwards compatibility but may not work
111
+ with new tenants where ACS app-only is disabled.
112
+
113
+ Returns:
114
+ Authenticated ClientContext
115
+ """
116
+ logger.warning(
117
+ "Using legacy ACS authentication. This may fail on new tenants. "
118
+ "Consider using MSAL or certificate-based auth instead."
119
+ )
120
+
121
+ credentials = ClientCredential(self.client_id, self.client_secret)
122
+ ctx = ClientContext(self.site_url).with_credentials(credentials)
123
+
124
+ return ctx
125
+
126
+ def get_context(self, auth_method: str = "msal") -> ClientContext:
127
+ """
128
+ Get authenticated ClientContext using the specified method.
129
+
130
+ Args:
131
+ auth_method: Authentication method to use.
132
+ Options: "msal" (default), "certificate", "legacy"
133
+
134
+ Returns:
135
+ Authenticated ClientContext
136
+
137
+ Raises:
138
+ ValueError: If invalid auth method specified
139
+ """
140
+ auth_methods = {
141
+ "msal": self.get_context_with_msal,
142
+ "certificate": self.get_context_with_certificate,
143
+ "legacy": self.get_context_legacy
144
+ }
145
+
146
+ if auth_method not in auth_methods:
147
+ raise ValueError(
148
+ f"Invalid auth method: {auth_method}. "
149
+ f"Must be one of: {', '.join(auth_methods.keys())}"
150
+ )
151
+
152
+ try:
153
+ return auth_methods[auth_method]()
154
+ except Exception as e:
155
+ logger.error(f"Authentication failed with method '{auth_method}': {e}")
156
+ raise
157
+
158
+
159
+ def create_sharepoint_context() -> ClientContext:
160
+ """
161
+ Factory function to create SharePoint context from environment variables.
162
+ Tries modern authentication methods first, falls back to legacy if needed.
163
+
164
+ Environment variables required:
165
+ - SHP_SITE_URL: SharePoint site URL
166
+ - SHP_ID_APP: Azure AD application client ID
167
+ - SHP_ID_APP_SECRET: Azure AD application client secret
168
+ - SHP_TENANT_ID: Azure AD tenant ID
169
+
170
+ Optional environment variables:
171
+ - SHP_AUTH_METHOD: Authentication method (msal, certificate, legacy)
172
+ - SHP_CERT_PATH: Path to certificate file
173
+ - SHP_CERT_THUMBPRINT: Certificate thumbprint
174
+
175
+ Returns:
176
+ Authenticated ClientContext
177
+
178
+ Raises:
179
+ ValueError: If required environment variables are missing
180
+ """
181
+ # Get required environment variables
182
+ site_url = os.getenv("SHP_SITE_URL")
183
+ client_id = os.getenv("SHP_ID_APP")
184
+ client_secret = os.getenv("SHP_ID_APP_SECRET")
185
+ tenant_id = os.getenv("SHP_TENANT_ID")
186
+
187
+ # Validate required variables
188
+ missing_vars = []
189
+ if not site_url:
190
+ missing_vars.append("SHP_SITE_URL")
191
+ if not client_id:
192
+ missing_vars.append("SHP_ID_APP")
193
+ if not client_secret:
194
+ missing_vars.append("SHP_ID_APP_SECRET")
195
+ if not tenant_id:
196
+ missing_vars.append("SHP_TENANT_ID")
197
+
198
+ if missing_vars:
199
+ raise ValueError(
200
+ f"Missing required environment variables: {', '.join(missing_vars)}"
201
+ )
202
+
203
+ # Get optional environment variables
204
+ auth_method = os.getenv("SHP_AUTH_METHOD", "msal")
205
+ cert_path = os.getenv("SHP_CERT_PATH")
206
+ cert_thumbprint = os.getenv("SHP_CERT_THUMBPRINT")
207
+
208
+ # Create authenticator
209
+ authenticator = SharePointAuthenticator(
210
+ site_url=site_url,
211
+ client_id=client_id,
212
+ client_secret=client_secret,
213
+ tenant_id=tenant_id,
214
+ cert_path=cert_path,
215
+ cert_thumbprint=cert_thumbprint
216
+ )
217
+
218
+ # Try to authenticate
219
+ try:
220
+ ctx = authenticator.get_context(auth_method=auth_method)
221
+ logger.info(f"Successfully created SharePoint context using {auth_method} auth")
222
+ return ctx
223
+ except Exception as e:
224
+ logger.error(f"Failed to create SharePoint context: {e}")
225
+
226
+ # If MSAL failed and we haven't tried legacy, suggest it
227
+ if auth_method == "msal":
228
+ logger.info(
229
+ "MSAL authentication failed. If you're using an older tenant, "
230
+ "you can try setting SHP_AUTH_METHOD=legacy, but note that "
231
+ "legacy ACS authentication is deprecated and may not work on new tenants."
232
+ )
233
+
234
+ raise
@@ -0,0 +1,359 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-sharepoint-us
3
+ Version: 2.0.0
4
+ Summary: SharePoint MCP Server with Modern Azure AD Authentication
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/mdev26/mcp-sharepoint
7
+ Project-URL: Repository, https://github.com/mdev26/mcp-sharepoint
8
+ Project-URL: Bug Tracker, https://github.com/mdev26/mcp-sharepoint/issues
9
+ Keywords: mcp,sharepoint,microsoft,azure,office365,msal
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: mcp>=1.0.0
21
+ Requires-Dist: office365-rest-python-client>=2.5.0
22
+ Requires-Dist: msal>=1.24.0
23
+ Requires-Dist: python-dotenv>=1.0.0
24
+ Requires-Dist: pydantic>=2.0.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
27
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
28
+ Requires-Dist: black>=23.0.0; extra == "dev"
29
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # SharePoint MCP Server - Updated with Modern Authentication
33
+
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
35
+
36
+ A comprehensive MCP Server for seamless integration with Microsoft SharePoint, **now with modern Azure AD authentication support** to work with new tenants.
37
+
38
+ ## 🆕 What's New in v2.0
39
+
40
+ This is an updated fork of the original [mcp-sharepoint](https://github.com/Sofias-ai/mcp-sharepoint) that fixes the `Acquire app-only access token failed` error by implementing modern Azure AD authentication methods.
41
+
42
+ ### Key Updates
43
+
44
+ - ✅ **Modern MSAL Authentication**: Uses Microsoft Authentication Library (MSAL) for Azure AD
45
+ - ✅ **Multiple Auth Methods**: Supports MSAL, certificate-based, and legacy authentication
46
+ - ✅ **New Tenant Compatible**: Works out-of-the-box with new Microsoft 365 tenants
47
+ - ✅ **Better Error Messages**: Clear guidance when authentication fails
48
+ - ✅ **Tenant ID Required**: Properly implements Azure AD app-only authentication
49
+
50
+ ## 🔧 The Problem This Fixes
51
+
52
+ The original mcp-sharepoint used the deprecated ACS (Azure Access Control Service) authentication method with `with_client_credentials()`. This fails on new tenants with:
53
+
54
+ ```
55
+ ValueError: Acquire app-only access token failed
56
+ ```
57
+
58
+ **Why this happens:**
59
+ - Microsoft disabled ACS app-only authentication by default for new tenants
60
+ - The old method doesn't include tenant ID in the authentication flow
61
+ - Modern Azure AD authentication requires MSAL or certificate-based auth
62
+
63
+ **This update solves it by:**
64
+ - Using MSAL (Microsoft Authentication Library) by default
65
+ - Properly passing tenant ID to Azure AD
66
+ - Supporting multiple modern authentication methods
67
+ - Falling back gracefully with helpful error messages
68
+
69
+ ## 📋 Prerequisites
70
+
71
+ ### Azure AD App Registration
72
+
73
+ You need to register an application in Azure AD with the following:
74
+
75
+ 1. **Go to Azure Portal** → Azure Active Directory → App registrations
76
+ 2. **Create new registration**:
77
+ - Name: "SharePoint MCP Server" (or your choice)
78
+ - Supported account types: "Accounts in this organizational directory only"
79
+ - Redirect URI: Not needed for app-only auth
80
+
81
+ 3. **Configure API Permissions**:
82
+ - Click "API permissions" → "Add a permission"
83
+ - Choose "SharePoint" → "Application permissions"
84
+ - Add these permissions:
85
+ - `Sites.Read.All` (for read operations)
86
+ - `Sites.ReadWrite.All` (for write operations)
87
+ - **Important**: Click "Grant admin consent" for your organization
88
+
89
+ 4. **Create Client Secret**:
90
+ - Go to "Certificates & secrets"
91
+ - Click "New client secret"
92
+ - Choose expiration (recommend 24 months)
93
+ - **Save the secret value immediately** (you won't see it again)
94
+
95
+ 5. **Note these values**:
96
+ - Application (client) ID
97
+ - Directory (tenant) ID
98
+ - Client secret value
99
+
100
+ ### Required Information
101
+
102
+ You'll need:
103
+ - **Tenant ID**: Your Azure AD tenant ID (GUID)
104
+ - **Client ID**: Your Azure AD application ID (GUID)
105
+ - **Client Secret**: The secret value you created
106
+ - **Site URL**: Your SharePoint site URL (e.g., `https://contoso.sharepoint.com/sites/yoursite`)
107
+
108
+ ## 🚀 Installation
109
+
110
+ ### From Source (Recommended for this fork)
111
+
112
+ ```bash
113
+ # Clone this repository
114
+ git clone https://github.com/your-username/mcp-sharepoint-updated.git
115
+ cd mcp-sharepoint-updated
116
+
117
+ # Install in development mode
118
+ pip install -e .
119
+ ```
120
+
121
+ ### Using uv (Alternative)
122
+
123
+ ```bash
124
+ uv pip install -e .
125
+ ```
126
+
127
+ ## ⚙️ Configuration
128
+
129
+ ### Environment Variables
130
+
131
+ Create a `.env` file or set these environment variables:
132
+
133
+ ```bash
134
+ # Required - Modern Authentication
135
+ SHP_TENANT_ID=your-tenant-id-guid
136
+ SHP_ID_APP=your-client-id-guid
137
+ SHP_ID_APP_SECRET=your-client-secret
138
+ SHP_SITE_URL=https://your-tenant.sharepoint.com/sites/your-site
139
+
140
+ # Optional
141
+ SHP_DOC_LIBRARY=Shared Documents
142
+ SHP_AUTH_METHOD=msal # Options: msal (default), certificate, legacy
143
+
144
+ # For certificate-based authentication (optional)
145
+ # SHP_CERT_PATH=/path/to/certificate.pem
146
+ # SHP_CERT_THUMBPRINT=your-cert-thumbprint
147
+ ```
148
+
149
+ ### Authentication Methods
150
+
151
+ The server supports three authentication methods:
152
+
153
+ #### 1. MSAL (Recommended - Default)
154
+ Modern Azure AD authentication using MSAL. Works with new tenants.
155
+ ```bash
156
+ SHP_AUTH_METHOD=msal
157
+ ```
158
+
159
+ #### 2. Certificate-Based
160
+ For organizations requiring certificate authentication.
161
+ ```bash
162
+ SHP_AUTH_METHOD=certificate
163
+ SHP_CERT_PATH=/path/to/cert.pem
164
+ SHP_CERT_THUMBPRINT=your-thumbprint
165
+ ```
166
+
167
+ #### 3. Legacy (Deprecated)
168
+ Old ACS authentication. Only use if you have an older tenant with ACS enabled.
169
+ ```bash
170
+ SHP_AUTH_METHOD=legacy
171
+ ```
172
+
173
+ ## 🔌 Claude Desktop Integration
174
+
175
+ ### Windows
176
+ Edit: `%APPDATA%\Claude\claude_desktop_config.json`
177
+
178
+ ### macOS
179
+ Edit: `~/Library/Application Support/Claude/claude_desktop_config.json`
180
+
181
+ ### Configuration
182
+
183
+ ```json
184
+ {
185
+ "mcpServers": {
186
+ "sharepoint": {
187
+ "command": "python",
188
+ "args": ["-m", "mcp_sharepoint"],
189
+ "env": {
190
+ "SHP_TENANT_ID": "your-tenant-id",
191
+ "SHP_ID_APP": "your-client-id",
192
+ "SHP_ID_APP_SECRET": "your-client-secret",
193
+ "SHP_SITE_URL": "https://your-tenant.sharepoint.com/sites/your-site",
194
+ "SHP_DOC_LIBRARY": "Shared Documents",
195
+ "SHP_AUTH_METHOD": "msal"
196
+ }
197
+ }
198
+ }
199
+ }
200
+ ```
201
+
202
+ ### Using uvx (Alternative)
203
+
204
+ ```json
205
+ {
206
+ "mcpServers": {
207
+ "sharepoint": {
208
+ "command": "uvx",
209
+ "args": ["mcp-sharepoint"],
210
+ "env": {
211
+ "SHP_TENANT_ID": "your-tenant-id",
212
+ "SHP_ID_APP": "your-client-id",
213
+ "SHP_ID_APP_SECRET": "your-client-secret",
214
+ "SHP_SITE_URL": "https://your-tenant.sharepoint.com/sites/your-site",
215
+ "SHP_AUTH_METHOD": "msal"
216
+ }
217
+ }
218
+ }
219
+ }
220
+ ```
221
+
222
+ ## 🧪 Testing the Connection
223
+
224
+ After installation, you can test the connection:
225
+
226
+ ```bash
227
+ # Set your environment variables first
228
+ export SHP_TENANT_ID=your-tenant-id
229
+ export SHP_ID_APP=your-client-id
230
+ export SHP_ID_APP_SECRET=your-secret
231
+ export SHP_SITE_URL=https://your-site.sharepoint.com/sites/yoursite
232
+
233
+ # Run the server
234
+ python -m mcp_sharepoint
235
+ ```
236
+
237
+ In Claude Desktop, you can use the "Test_Connection" tool to verify everything is working.
238
+
239
+ ## 🛠️ Available Tools
240
+
241
+ The server provides these tools for SharePoint operations:
242
+
243
+ ### Document Management
244
+ - `List_SharePoint_Documents` - List all documents in a folder
245
+ - `Get_Document_Content` - Read document content
246
+ - `Upload_Document` - Upload new documents
247
+ - `Update_Document` - Update existing documents
248
+ - `Delete_Document` - Delete documents
249
+
250
+ ### Folder Management
251
+ - `List_SharePoint_Folders` - List folders in a directory
252
+ - `Create_Folder` - Create new folders
253
+ - `Delete_Folder` - Delete empty folders
254
+ - `Get_SharePoint_Tree` - Get recursive folder structure
255
+
256
+ ### Utilities
257
+ - `Test_Connection` - Test authentication and connection
258
+
259
+ ## 🔍 Troubleshooting
260
+
261
+ ### "Acquire app-only access token failed"
262
+
263
+ **This is the error we're fixing!** If you still see this:
264
+
265
+ 1. **Check you're using MSAL**:
266
+ ```bash
267
+ SHP_AUTH_METHOD=msal # Make sure this is set
268
+ ```
269
+
270
+ 2. **Verify tenant ID is correct**:
271
+ - Go to Azure Portal → Azure Active Directory → Overview
272
+ - Copy the "Tenant ID" GUID
273
+ - Ensure it matches `SHP_TENANT_ID`
274
+
275
+ 3. **Check API permissions**:
276
+ - Azure Portal → Your App → API permissions
277
+ - Ensure SharePoint permissions are granted
278
+ - Click "Grant admin consent"
279
+
280
+ 4. **Wait for permission propagation**:
281
+ - After granting permissions, wait 5-10 minutes
282
+ - Try again
283
+
284
+ ### "Authentication failed"
285
+
286
+ 1. **Verify credentials**:
287
+ - Client ID is correct (from App registrations → Overview)
288
+ - Client secret is correct and not expired
289
+ - Tenant ID is correct
290
+
291
+ 2. **Check site URL**:
292
+ - Must be exact SharePoint site URL
293
+ - Should NOT end with a slash
294
+ - Example: `https://contoso.sharepoint.com/sites/marketing`
295
+
296
+ 3. **Verify network access**:
297
+ - Ensure you can reach `login.microsoftonline.com` on port 443
298
+ - Check firewall/proxy settings
299
+
300
+ ### "Access denied" / "403 Forbidden"
301
+
302
+ 1. **Check SharePoint permissions**:
303
+ - API permissions must be **Application** permissions, not Delegated
304
+ - Need `Sites.Read.All` or `Sites.ReadWrite.All`
305
+ - Admin consent must be granted
306
+
307
+ 2. **Verify site access**:
308
+ - The app must have access to the specific SharePoint site
309
+ - May need to grant site permissions separately
310
+
311
+ ## 📚 Migration from Original mcp-sharepoint
312
+
313
+ If you're migrating from the original version:
314
+
315
+ 1. **Add new environment variable**:
316
+ ```bash
317
+ SHP_TENANT_ID=your-tenant-id # This is new!
318
+ ```
319
+
320
+ 2. **Optional: Set auth method explicitly**:
321
+ ```bash
322
+ SHP_AUTH_METHOD=msal # Explicitly use modern auth
323
+ ```
324
+
325
+ 3. **Update your Claude Desktop config**:
326
+ - Add `SHP_TENANT_ID` to the env section
327
+ - Optionally add `SHP_AUTH_METHOD: "msal"`
328
+
329
+ 4. **Restart Claude Desktop** to load the new configuration
330
+
331
+ ## 🤝 Contributing
332
+
333
+ Contributions are welcome! Please feel free to submit a Pull Request.
334
+
335
+ ## 📄 License
336
+
337
+ This project is licensed under the MIT License - see the LICENSE file for details.
338
+
339
+ ## 🙏 Acknowledgments
340
+
341
+ - Original [mcp-sharepoint](https://github.com/Sofias-ai/mcp-sharepoint) by sofias tech
342
+ - [Office365-REST-Python-Client](https://github.com/vgrem/Office365-REST-Python-Client) library
343
+ - Microsoft Authentication Library (MSAL) for Python
344
+
345
+ ## 📞 Support
346
+
347
+ If you encounter issues:
348
+
349
+ 1. Check the [Troubleshooting](#-troubleshooting) section
350
+ 2. Review [Azure AD App Setup](#azure-ad-app-registration)
351
+ 3. Open an issue with:
352
+ - Error message (sanitize any secrets!)
353
+ - Your environment (Python version, OS)
354
+ - Authentication method being used
355
+ - Whether it's a new or existing tenant
356
+
357
+ ---
358
+
359
+ **Note**: This is an updated version that solves the authentication issues with new Microsoft 365 tenants. The original version can be found at [Sofias-ai/mcp-sharepoint](https://github.com/Sofias-ai/mcp-sharepoint).
@@ -0,0 +1,9 @@
1
+ mcp_sharepoint/__init__.py,sha256=qmZseIbfu-kd0SsT6400uXbuwDCszDly4C5VlGKuMIw,19762
2
+ mcp_sharepoint/__main__.py,sha256=HAV6LtLss9FL5CdMcp3ssbaDe-i669Vsc90sqXCB7Mk,140
3
+ mcp_sharepoint/auth.py,sha256=ihQmvPViAA5jwNNFZt3c0yAfScTADiQM1VmYpTM1EHI,8383
4
+ mcp_sharepoint_us-2.0.0.dist-info/licenses/LICENSE,sha256=SRM8juGH4GjIqnl5rrp-P-S5mW5h2mINOPx5-wOZG6s,1112
5
+ mcp_sharepoint_us-2.0.0.dist-info/METADATA,sha256=0DE6DJCXlI3riuwQ6gZyQbr0e8s91ZLUt9hEGvipQGA,11370
6
+ mcp_sharepoint_us-2.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
7
+ mcp_sharepoint_us-2.0.0.dist-info/entry_points.txt,sha256=B5hvvayyuKrvH1DvZtsnZC4oWAh-agQfUByY0LvxUgk,55
8
+ mcp_sharepoint_us-2.0.0.dist-info/top_level.txt,sha256=R6mRoWe61lz4kUSKGV6S2XVbE7825xfC_J-ouZIYpuo,15
9
+ mcp_sharepoint_us-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mcp-sharepoint = mcp_sharepoint:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 SharePoint MCP Server Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ mcp_sharepoint