365center-mcp 1.1.0 → 1.2.0

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 365center-mcp
2
2
 
3
- **MCP server for Microsoft 365 / SharePoint — 33 tools for full read/write access**
3
+ **MCP server for Microsoft 365 / SharePoint — 35 tools for full read/write access**
4
4
 
5
5
  Available on [GitHub](https://github.com/Crscristi28/365center-mcp) · [npm](https://www.npmjs.com/package/365center-mcp) · [Docker Hub](https://hub.docker.com/r/crscristi28/365center-mcp) · [cristianb.cz](https://cristianb.cz)
6
6
 
@@ -9,7 +9,7 @@ Available on [GitHub](https://github.com/Crscristi28/365center-mcp) · [npm](htt
9
9
  [![License](https://img.shields.io/badge/license-BUSL--1.1-blue.svg)](https://github.com/Crscristi28/365center-mcp/blob/main/LICENSE)
10
10
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
11
11
 
12
- Full visual walkthrough: **[Setup Guide PDF](SETUP-GUIDE.pdf)** — screenshots for every Azure setup step and all 3 installation methods.
12
+ Full visual walkthrough: **[Setup Guide PDF](https://github.com/Crscristi28/365center-mcp/blob/main/SETUP-GUIDE.pdf)** — screenshots for every Azure setup step and all 3 installation methods.
13
13
 
14
14
  ---
15
15
 
@@ -42,7 +42,7 @@ Full visual walkthrough: **[Setup Guide PDF](SETUP-GUIDE.pdf)** — screenshots
42
42
 
43
43
  `365center-mcp` is a Model Context Protocol (MCP) server that gives Claude — and any other MCP-compatible AI client — full read/write access to Microsoft 365 SharePoint sites.
44
44
 
45
- It exposes **33 tools** covering SharePoint sites, document libraries, documents, pages, metadata columns, navigation, and permissions. Claude can list sites, upload and download files, tag documents, create and publish pages, build navigation menus, manage permissions, and more — all through a single MCP connection.
45
+ It exposes **35 tools** covering SharePoint sites, document libraries, documents, pages, metadata columns, navigation, and permissions. Claude can list sites, upload and download files, tag documents, create and publish pages, build navigation menus, manage permissions, and more — all through a single MCP connection.
46
46
 
47
47
  Built for manufacturing companies managing factory documentation in SharePoint, but works with any Microsoft 365 tenant.
48
48
 
@@ -56,17 +56,19 @@ Built for manufacturing companies managing factory documentation in SharePoint,
56
56
 
57
57
  ## Features
58
58
 
59
- **33 tools** across 7 categories. All tools use Microsoft Graph API or SharePoint REST API — no middlemen.
59
+ **35 tools** across 7 categories. All tools use Microsoft Graph API or SharePoint REST API — no middlemen.
60
60
 
61
- ### Sites (3 tools)
61
+ ### Sites (4 tools)
62
62
  - `list_sites` — List all SharePoint sites in the tenant
63
63
  - `get_site` — Get site by URL
64
64
  - `get_site_by_id` — Get site by ID
65
+ - `create_site` — Create a new Communication or Team site
65
66
 
66
- ### Documents (8 tools)
67
+ ### Documents (9 tools)
67
68
  - `list_document_libraries` — List document libraries (drives)
68
69
  - `list_documents` — List documents with both driveItemId and listItemId
69
- - `upload_document` — Upload files to SharePoint
70
+ - `upload_document` — Upload a file to SharePoint (auto session upload for files over 4 MB)
71
+ - `upload_documents` — Upload multiple files with optional metadata in one call (max 30 per call)
70
72
  - `download_document` — Download files from SharePoint to a local path
71
73
  - `search_documents` — Search across documents
72
74
  - `delete_document` — Delete a document
@@ -227,7 +229,7 @@ Replace the four `your-*` values with what you collected in Step 8 of the Azure
227
229
 
228
230
  On Windows, use `C:\\Users\\YOUR_USERNAME\\.365center-mcp:/home/mcp/.365center-mcp` as the volume mount.
229
231
 
230
- **4. Open Claude Desktop.** The server will appear in the MCP menu with all 33 tools loaded.
232
+ **4. Open Claude Desktop.** The server will appear in the MCP menu with all 35 tools loaded.
231
233
 
232
234
  > **Why the volume mount?** The server caches delegated auth tokens in `~/.365center-mcp/token-cache.json`. Without the volume, you would need to re-authenticate every time Docker restarts.
233
235
 
package/dist/index.js CHANGED
@@ -3,11 +3,12 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
  import { listSites, getSite, getSiteById } from "./tools/sites.js";
6
- import { listDocumentLibraries, listDocuments, uploadDocument, downloadDocument, searchDocuments, deleteDocument, createFolder, getDocumentVersions } from "./tools/documents.js";
6
+ import { listDocumentLibraries, listDocuments, uploadDocument, uploadDocuments, downloadDocument, searchDocuments, deleteDocument, createFolder, getDocumentVersions } from "./tools/documents.js";
7
7
  import { listColumns, createChoiceColumn, createTextColumn, setDocumentMetadata, getDocumentMetadata } from "./tools/metadata.js";
8
8
  import { listPages, createPage, createPageWithContent, addQuickLinksWebPart, publishPage, deletePage } from "./tools/pages.js";
9
9
  import { getNavigation, addNavigationLink, deleteNavigationLink } from "./tools/navigation.js";
10
10
  import { getPageCanvasContent, setPageCanvasContent, copyPage, listSitePages } from "./tools/pages-rest.js";
11
+ import { createSite } from "./tools/sites-rest.js";
11
12
  import { getSitePermissions, getGroupMembers, addUserToGroup, removeUserFromGroup } from "./tools/permissions.js";
12
13
  const server = new McpServer({
13
14
  name: "365center-mcp",
@@ -26,6 +27,14 @@ server.tool("get_site_by_id", "Get a SharePoint site by its ID. Site ID format:
26
27
  const site = await getSiteById(siteId);
27
28
  return { content: [{ type: "text", text: JSON.stringify(site, null, 2) }] };
28
29
  });
30
+ server.tool("create_site", "Create a new SharePoint site. Template: 'communication' (default) for publishing/documentation sites, 'team' for collaboration. Returns siteId for use in all other tools. Site creation takes a few seconds. Uses delegated auth (device code flow on first use).", {
31
+ title: z.string().describe("Display name for the new site"),
32
+ urlSlug: z.string().describe("URL slug — the site will be at https://{domain}/sites/{urlSlug}"),
33
+ template: z.enum(["communication", "team"]).optional().describe("Site template: 'communication' (default) or 'team'"),
34
+ }, async ({ title, urlSlug, template }) => {
35
+ const result = await createSite(title, urlSlug, template || "communication");
36
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
37
+ });
29
38
  // ============ DOCUMENTS ============
30
39
  server.tool("list_document_libraries", "List all document libraries (drives) in a SharePoint site. Returns driveId for each library — use driveId in list_documents, upload_document, delete_document, and other document tools. Also returns listId for metadata operations.", { siteId: z.string().describe("SharePoint site ID") }, async ({ siteId }) => {
31
40
  const libraries = await listDocumentLibraries(siteId);
@@ -39,7 +48,7 @@ server.tool("list_documents", "List documents in a document library folder. Retu
39
48
  const docs = await listDocuments(siteId, driveId, folderId || "root");
40
49
  return { content: [{ type: "text", text: JSON.stringify(docs, null, 2) }] };
41
50
  });
42
- server.tool("upload_document", "Upload a local file to a SharePoint document library. File is uploaded to the root folder by default, or to a specific folder if folderId is provided. After upload, use set_document_metadata to set metadata fields on the document.", {
51
+ server.tool("upload_document", "Upload a local file to a SharePoint document library. File is uploaded to the root folder by default, or to a specific folder if folderId is provided. After upload, use set_document_metadata to set metadata fields on the document. Supports files up to 250 GB (automatic session upload for files over 4 MB).", {
43
52
  siteId: z.string().describe("SharePoint site ID"),
44
53
  driveId: z.string().describe("Document library (drive) ID"),
45
54
  fileName: z.string().describe("Name for the file in SharePoint"),
@@ -58,6 +67,23 @@ server.tool("download_document", "Download a document from a SharePoint document
58
67
  const result = await downloadDocument(siteId, driveId, itemId, localPath);
59
68
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
60
69
  });
70
+ server.tool("upload_documents", "Upload one or more files to SharePoint, optionally setting metadata on each. Max 30 files per call. Files over 4 MB use resumable upload automatically. 500ms delay between files for API rate limits. Returns per-file status for upload and metadata independently.", {
71
+ siteId: z.string().describe("SharePoint site ID"),
72
+ driveId: z.string().describe("Document library (drive) ID"),
73
+ listId: z.string().optional().describe("List ID — required only if setting metadata via fields"),
74
+ folderId: z.string().optional().describe("Target folder ID (default: root)"),
75
+ files: z.array(z.object({
76
+ fileName: z.string().describe("Name for the file in SharePoint"),
77
+ filePath: z.string().describe("Local file path to upload"),
78
+ fields: z.string().optional().describe("JSON string of metadata fields to set after upload, e.g. '{\"Oblast\":\"Linka1\",\"WS\":\"WS9\"}'"),
79
+ })).describe("Array of files to upload (max 30)"),
80
+ }, async ({ siteId, driveId, listId, folderId, files }) => {
81
+ if (files.length > 30) {
82
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Maximum 30 files per call. Split into multiple calls." }) }] };
83
+ }
84
+ const results = await uploadDocuments(siteId, driveId, listId, files, folderId || "root");
85
+ return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
86
+ });
61
87
  server.tool("search_documents", "Search for documents in a SharePoint site", {
62
88
  siteId: z.string().describe("SharePoint site ID"),
63
89
  query: z.string().describe("Search query"),
@@ -24,3 +24,15 @@ export declare function createFolder(siteId: string, driveId: string, folderName
24
24
  url: any;
25
25
  }>;
26
26
  export declare function getDocumentVersions(siteId: string, driveId: string, itemId: string): Promise<any>;
27
+ export declare function uploadDocuments(siteId: string, driveId: string, listId: string | undefined, files: Array<{
28
+ fileName: string;
29
+ filePath: string;
30
+ fields?: string;
31
+ }>, folderId?: string): Promise<{
32
+ fileName: string;
33
+ status: "ok" | "error";
34
+ id?: string;
35
+ error?: string;
36
+ metadataStatus?: "ok" | "error" | "skipped";
37
+ metadataError?: string;
38
+ }[]>;
@@ -1,4 +1,5 @@
1
1
  import { graphClient } from "../auth.js";
2
+ import { setDocumentMetadata } from "./metadata.js";
2
3
  import fs from "fs";
3
4
  import path from "path";
4
5
  import { Readable } from "stream";
@@ -30,15 +31,62 @@ export async function listDocuments(siteId, driveId, folderId = "root") {
30
31
  }));
31
32
  }
32
33
  export async function uploadDocument(siteId, driveId, fileName, filePath, folderId = "root") {
33
- const fileContent = fs.readFileSync(filePath);
34
- const result = await graphClient
35
- .api(`/sites/${siteId}/drives/${driveId}/items/${folderId}:/${fileName}:/content`)
36
- .putStream(fileContent);
34
+ const fileSize = fs.statSync(filePath).size;
35
+ const FOUR_MB = 4 * 1024 * 1024;
36
+ if (fileSize <= FOUR_MB) {
37
+ const fileContent = fs.readFileSync(filePath);
38
+ const result = await graphClient
39
+ .api(`/sites/${siteId}/drives/${driveId}/items/${folderId}:/${fileName}:/content`)
40
+ .putStream(fileContent);
41
+ return {
42
+ id: result.id,
43
+ name: result.name,
44
+ url: result.webUrl,
45
+ size: result.size,
46
+ };
47
+ }
48
+ const session = await graphClient
49
+ .api(`/sites/${siteId}/drives/${driveId}/items/${folderId}:/${fileName}:/createUploadSession`)
50
+ .post({
51
+ item: { "@microsoft.graph.conflictBehavior": "rename" },
52
+ });
53
+ const uploadUrl = session.uploadUrl;
54
+ const CHUNK_SIZE = 10 * 1024 * 1024;
55
+ const fd = fs.openSync(filePath, "r");
56
+ let offset = 0;
57
+ let finalResult;
58
+ try {
59
+ while (offset < fileSize) {
60
+ const length = Math.min(CHUNK_SIZE, fileSize - offset);
61
+ const buffer = Buffer.alloc(length);
62
+ fs.readSync(fd, buffer, 0, length, offset);
63
+ const end = offset + length - 1;
64
+ const response = await fetch(uploadUrl, {
65
+ method: "PUT",
66
+ headers: {
67
+ "Content-Length": length.toString(),
68
+ "Content-Range": `bytes ${offset}-${end}/${fileSize}`,
69
+ },
70
+ body: buffer,
71
+ });
72
+ if (response.status === 200 || response.status === 201) {
73
+ finalResult = await response.json();
74
+ }
75
+ else if (response.status !== 202) {
76
+ const errorText = await response.text();
77
+ throw new Error(`Upload chunk failed: ${response.status} ${errorText}`);
78
+ }
79
+ offset += length;
80
+ }
81
+ }
82
+ finally {
83
+ fs.closeSync(fd);
84
+ }
37
85
  return {
38
- id: result.id,
39
- name: result.name,
40
- url: result.webUrl,
41
- size: result.size,
86
+ id: finalResult.id,
87
+ name: finalResult.name,
88
+ url: finalResult.webUrl,
89
+ size: finalResult.size,
42
90
  };
43
91
  }
44
92
  export async function downloadDocument(siteId, driveId, itemId, localPath) {
@@ -122,3 +170,35 @@ export async function getDocumentVersions(siteId, driveId, itemId) {
122
170
  size: v.size,
123
171
  }));
124
172
  }
173
+ export async function uploadDocuments(siteId, driveId, listId, files, folderId = "root") {
174
+ const results = [];
175
+ for (const file of files) {
176
+ const entry = { fileName: file.fileName, status: "ok" };
177
+ try {
178
+ const uploaded = await uploadDocument(siteId, driveId, file.fileName, file.filePath, folderId);
179
+ entry.id = uploaded.id;
180
+ if (file.fields && listId) {
181
+ try {
182
+ const fields = JSON.parse(file.fields);
183
+ await setDocumentMetadata(siteId, listId, uploaded.id, fields, driveId);
184
+ entry.metadataStatus = "ok";
185
+ }
186
+ catch (metaErr) {
187
+ entry.metadataStatus = "error";
188
+ entry.metadataError = metaErr.message || String(metaErr);
189
+ }
190
+ }
191
+ else {
192
+ entry.metadataStatus = "skipped";
193
+ }
194
+ }
195
+ catch (err) {
196
+ entry.status = "error";
197
+ entry.error = err.message || String(err);
198
+ entry.metadataStatus = "skipped";
199
+ }
200
+ results.push(entry);
201
+ await new Promise((r) => setTimeout(r, 500));
202
+ }
203
+ return results;
204
+ }
@@ -1,4 +1,4 @@
1
- import { graphClient } from "../auth.js";
1
+ import { graphClient, callSharePointRest } from "../auth.js";
2
2
  // Translate drive item ID to list item ID if needed
3
3
  async function resolveListItemId(siteId, listId, itemId, driveId) {
4
4
  // If it's already a numeric ID, use it directly
@@ -39,26 +39,41 @@ export async function listColumns(siteId, listId) {
39
39
  }));
40
40
  }
41
41
  export async function createChoiceColumn(siteId, listId, name, displayName, choices, allowMultiple = false) {
42
- const columnDef = {
43
- name,
44
- displayName,
45
- choice: {
46
- allowTextEntry: false,
47
- choices,
48
- displayAs: allowMultiple ? "checkBoxes" : "dropDownMenu",
49
- },
50
- };
51
- if (allowMultiple) {
52
- columnDef.indexed = false;
42
+ if (!allowMultiple) {
43
+ const columnDef = {
44
+ name,
45
+ displayName,
46
+ choice: {
47
+ allowTextEntry: false,
48
+ choices,
49
+ displayAs: "dropDownMenu",
50
+ },
51
+ };
52
+ const result = await graphClient
53
+ .api(`/sites/${siteId}/lists/${listId}/columns`)
54
+ .post(columnDef);
55
+ return {
56
+ id: result.id,
57
+ name: result.name,
58
+ displayName: result.displayName,
59
+ allowMultiple: false,
60
+ };
53
61
  }
54
- const result = await graphClient
55
- .api(`/sites/${siteId}/lists/${listId}/columns`)
56
- .post(columnDef);
62
+ const site = await graphClient.api(`/sites/${siteId}`).select("webUrl").get();
63
+ const siteUrl = site.webUrl;
64
+ const result = await callSharePointRest(siteUrl, `/_api/web/lists/getByTitle('${listId}')/fields`, "POST", {
65
+ __metadata: { type: "SP.FieldMultiChoice" },
66
+ FieldTypeKind: 15,
67
+ Title: displayName,
68
+ StaticName: name,
69
+ InternalName: name,
70
+ Choices: { results: choices },
71
+ });
57
72
  return {
58
- id: result.id,
59
- name: result.name,
60
- displayName: result.displayName,
61
- allowMultiple,
73
+ id: result.d?.Id || result.Id,
74
+ name: name,
75
+ displayName: displayName,
76
+ allowMultiple: true,
62
77
  };
63
78
  }
64
79
  export async function createTextColumn(siteId, listId, name, displayName) {
@@ -80,9 +95,19 @@ export async function createTextColumn(siteId, listId, name, displayName) {
80
95
  }
81
96
  export async function setDocumentMetadata(siteId, listId, itemId, fields, driveId) {
82
97
  const resolvedId = await resolveListItemId(siteId, listId, itemId, driveId);
98
+ const patchFields = {};
99
+ for (const [key, value] of Object.entries(fields)) {
100
+ if (Array.isArray(value)) {
101
+ patchFields[`${key}@odata.type`] = "Collection(Edm.String)";
102
+ patchFields[key] = value;
103
+ }
104
+ else {
105
+ patchFields[key] = value;
106
+ }
107
+ }
83
108
  const result = await graphClient
84
109
  .api(`/sites/${siteId}/lists/${listId}/items/${resolvedId}/fields`)
85
- .patch(fields);
110
+ .patch(patchFields);
86
111
  return result;
87
112
  }
88
113
  export async function getDocumentMetadata(siteId, listId, itemId, driveId) {
@@ -0,0 +1,5 @@
1
+ export declare function createSite(title: string, urlSlug: string, template?: "communication" | "team"): Promise<{
2
+ siteId: any;
3
+ url: string;
4
+ title: string;
5
+ }>;
@@ -0,0 +1,21 @@
1
+ import { callSharePointRest } from "../auth.js";
2
+ export async function createSite(title, urlSlug, template = "communication") {
3
+ const domain = process.env.SHAREPOINT_DOMAIN;
4
+ const adminUrl = `https://${domain.replace(".sharepoint.com", "-admin.sharepoint.com")}`;
5
+ const siteUrl = `https://${domain}/sites/${urlSlug}`;
6
+ const webTemplate = template === "communication"
7
+ ? "SITEPAGEPUBLISHING#0"
8
+ : "STS#3";
9
+ const result = await callSharePointRest(adminUrl, "/_api/SPSiteManager/create", "POST", {
10
+ request: {
11
+ Title: title,
12
+ Url: siteUrl,
13
+ WebTemplate: webTemplate,
14
+ },
15
+ });
16
+ return {
17
+ siteId: result.d?.SiteId || result.SiteId,
18
+ url: siteUrl,
19
+ title: title,
20
+ };
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "365center-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server for Microsoft 365 / Office 365 SharePoint — 32 tools for full read-write access to sites, documents, pages, metadata, navigation, and permissions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",