mcp-sharepoint-us 2.0.2__py3-none-any.whl → 2.0.3__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.

@@ -1,539 +1,539 @@
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())
536
-
537
- def run():
538
- """Sync entry point for the package"""
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())
536
+
537
+ def run():
538
+ """Sync entry point for the package"""
539
539
  asyncio.run(main())