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.
- mcp_sharepoint/__init__.py +535 -0
- mcp_sharepoint/__main__.py +8 -0
- mcp_sharepoint/auth.py +234 -0
- mcp_sharepoint_us-2.0.0.dist-info/METADATA +359 -0
- mcp_sharepoint_us-2.0.0.dist-info/RECORD +9 -0
- mcp_sharepoint_us-2.0.0.dist-info/WHEEL +5 -0
- mcp_sharepoint_us-2.0.0.dist-info/entry_points.txt +2 -0
- mcp_sharepoint_us-2.0.0.dist-info/licenses/LICENSE +21 -0
- mcp_sharepoint_us-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -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())
|
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
|
+
[](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,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
|