365center-mcp 1.0.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/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ Business Source License 1.1
2
+
3
+ Licensor: Cristian Bucioacă
4
+ Licensed Work: 365center-mcp
5
+ Change Date: 2030-04-08
6
+ Change License: MIT
7
+
8
+ Terms
9
+
10
+ The Licensor hereby grants you the right to copy, modify, create derivative
11
+ works, redistribute, and make non-production use of the Licensed Work.
12
+
13
+ Additional Use Grant: You may use the Licensed Work for internal business
14
+ purposes, testing, development, and personal projects.
15
+
16
+ You may NOT use the Licensed Work to provide a commercial product or service
17
+ that competes with the Licensed Work without explicit written permission
18
+ from the Licensor.
19
+
20
+ On the Change Date, or the fourth anniversary of the first publicly available
21
+ distribution of a specific version of the Licensed Work under this License,
22
+ whichever comes first, the Licensor hereby grants you rights under the terms
23
+ of the Change License, and the rights granted in the paragraph above terminate.
24
+
25
+ Contact: info@cristianb.cz
package/README.md ADDED
@@ -0,0 +1,258 @@
1
+ # 365center-mcp
2
+
3
+ MCP server for Microsoft 365 and SharePoint. Gives Claude (and any MCP client) full read-write access to SharePoint sites, documents, pages, metadata, navigation, and permissions via Microsoft Graph API and SharePoint REST API.
4
+
5
+ Built for manufacturing companies that manage factory documentation in SharePoint.
6
+
7
+ ## Features
8
+
9
+ **32 tools** across 7 categories:
10
+
11
+ ### Sites
12
+ - `list_sites` — List all SharePoint sites in the tenant
13
+ - `get_site` — Get site by URL
14
+ - `get_site_by_id` — Get site by ID
15
+
16
+ ### Documents
17
+ - `list_document_libraries` — List document libraries (drives)
18
+ - `list_documents` — List documents with both driveItemId and listItemId
19
+ - `upload_document` — Upload files to SharePoint
20
+ - `search_documents` — Search across documents
21
+ - `delete_document` — Delete a document
22
+ - `create_folder` — Create folders
23
+ - `get_document_versions` — Version history (audit trail)
24
+
25
+ ### Metadata
26
+ - `list_columns` — List custom metadata columns
27
+ - `create_choice_column` — Create choice/dropdown columns
28
+ - `create_text_column` — Create text columns
29
+ - `get_document_metadata` — Read document metadata
30
+ - `set_document_metadata` — Set metadata on documents
31
+
32
+ ### Pages
33
+ - `list_pages` — List all pages
34
+ - `create_page` — Create empty page
35
+ - `create_page_with_content` — Create page with sections and HTML content
36
+ - `add_quick_links` — Add Quick Links web part
37
+ - `publish_page` — Publish a draft page
38
+ - `delete_page` — Delete a page
39
+
40
+ ### Pages (REST API)
41
+ - `list_site_pages` — List pages with numeric IDs
42
+ - `get_page_canvas_content` — Read raw page content (CanvasContent1)
43
+ - `set_page_canvas_content` — Write raw page content (supports Highlighted Content and any web part)
44
+ - `copy_page` — Copy a page as template
45
+
46
+ ### Navigation
47
+ - `get_navigation` — Read top navigation menu
48
+ - `add_navigation_link` — Add link to navigation
49
+ - `delete_navigation_link` — Remove link from navigation
50
+
51
+ ### Permissions
52
+ - `get_permissions` — List SharePoint groups (Visitors, Members, Owners)
53
+ - `get_group_members` — List members of a group
54
+ - `add_user_to_group` — Add user to a group
55
+ - `remove_user_from_group` — Remove user from a group
56
+
57
+ ## Authentication
58
+
59
+ 365center-mcp uses two authentication methods:
60
+
61
+ - **App-only (Client Credentials)** — for Graph API operations (sites, documents, pages, metadata). Works automatically with Azure App Registration credentials.
62
+ - **Delegated (Device Code Flow)** — for SharePoint REST API operations (navigation, permissions, Highlighted Content). On first use, you'll be prompted to sign in via https://login.microsoft.com/device with a one-time code. Token is cached and refreshed automatically.
63
+
64
+ ## Prerequisites
65
+
66
+ - Microsoft 365 tenant with SharePoint
67
+ - Azure App Registration with:
68
+ - **Application permissions:** Sites.ReadWrite.All, Sites.FullControl.All, Files.ReadWrite.All, Sites.Manage.All
69
+ - **Delegated permissions:** Sites.ReadWrite.All, Sites.FullControl.All, offline_access
70
+ - **SharePoint permissions:** Sites.FullControl.All
71
+ - **"Allow public client flows" enabled** (for device code auth)
72
+
73
+ ## Installation
74
+
75
+ ### Docker (recommended)
76
+
77
+ ```bash
78
+ docker pull crscristi28/365center-mcp:latest
79
+
80
+ # Claude Desktop config (claude_desktop_config.json):
81
+ {
82
+ "mcpServers": {
83
+ "<your-server-name>": {
84
+ "command": "docker",
85
+ "args": [
86
+ "run", "-i", "--rm",
87
+ "-e", "AZURE_TENANT_ID=your-tenant-id",
88
+ "-e", "AZURE_CLIENT_ID=your-client-id",
89
+ "-e", "AZURE_CLIENT_SECRET=your-client-secret",
90
+ "-e", "SHAREPOINT_DOMAIN=your-domain.sharepoint.com",
91
+ "-v", "~/.365center-mcp:/home/mcp/.365center-mcp",
92
+ "crscristi28/365center-mcp:latest"
93
+ ]
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ ### Node.js
100
+
101
+ ```bash
102
+ git clone https://github.com/Crscristi28/365center-mcp.git
103
+ cd 365center-mcp/mcp-server
104
+ npm install
105
+ npm run build
106
+
107
+ # Create .env file:
108
+ AZURE_TENANT_ID=your-tenant-id
109
+ AZURE_CLIENT_ID=your-client-id
110
+ AZURE_CLIENT_SECRET=your-client-secret
111
+ SHAREPOINT_DOMAIN=your-domain.sharepoint.com
112
+
113
+ # Run:
114
+ node dist/index.js
115
+ ```
116
+
117
+ ### Claude Desktop config (Node.js)
118
+
119
+ ```json
120
+ {
121
+ "mcpServers": {
122
+ "<your-server-name>": {
123
+ "command": "node",
124
+ "args": ["/path/to/mcp-server/dist/index.js"],
125
+ "env": {
126
+ "AZURE_TENANT_ID": "your-tenant-id",
127
+ "AZURE_CLIENT_ID": "your-client-id",
128
+ "AZURE_CLIENT_SECRET": "your-client-secret",
129
+ "SHAREPOINT_DOMAIN": "your-domain.sharepoint.com"
130
+ }
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ ## Page Layouts
137
+
138
+ When using `create_page_with_content`, available section layouts:
139
+
140
+ | Layout | Columns | Widths |
141
+ |--------|---------|--------|
142
+ | `oneColumn` | 1 | 12 |
143
+ | `twoColumns` | 2 | 6 + 6 |
144
+ | `threeColumns` | 3 | 4 + 4 + 4 |
145
+ | `oneThirdLeftColumn` | 2 | 4 + 8 |
146
+ | `oneThirdRightColumn` | 2 | 8 + 4 |
147
+ | `fullWidth` | 1 | 12 |
148
+
149
+ ## Supported Web Parts
150
+
151
+ When creating pages via Graph API, these standard web parts are supported:
152
+
153
+ | Web Part | Type ID |
154
+ |----------|---------|
155
+ | Bing Maps | `e377ea37-9047-43b9-8cdb-a761be2f8e09` |
156
+ | Button | `0f087d7f-520e-42b7-89c0-496aaf979d58` |
157
+ | Call To Action | `df8e44e7-edd5-46d5-90da-aca1539313b8` |
158
+ | Divider | `2161a1c6-db61-4731-b97c-3cdb303f7cbb` |
159
+ | Document Embed | `b7dd04e1-19ce-4b24-9132-b60a1c2b910d` |
160
+ | Image | `d1d91016-032f-456d-98a4-721247c305e8` |
161
+ | Image Gallery | `af8be689-990e-492a-81f7-ba3e4cd3ed9c` |
162
+ | Link Preview | `6410b3b6-d440-4663-8744-378976dc041e` |
163
+ | Org Chart | `e84a8ca2-f63c-4fb9-bc0b-d8eef5ccb22b` |
164
+ | People | `7f718435-ee4d-431c-bdbf-9c4ff326f46e` |
165
+ | Quick Links | `c70391ea-0b10-4ee9-b2b4-006d3fcad0cd` |
166
+ | Spacer | `8654b779-4886-46d4-8ffb-b5ed960ee986` |
167
+ | YouTube Embed | `544dd15b-cf3c-441b-96da-004d5a8cea1d` |
168
+ | Title Area | `cbe7b0a9-3504-44dd-a3a3-0e5cacd07788` |
169
+
170
+ For **Highlighted Content** and other unsupported web parts, use the REST API tools (`get_page_canvas_content` / `set_page_canvas_content`).
171
+
172
+ ## Security
173
+
174
+ 365center-mcp is designed for enterprise environments:
175
+
176
+ - **No data leaves your tenant** — all API calls go directly from the MCP server to Microsoft Graph API and SharePoint REST API. No third-party servers, no telemetry, no analytics.
177
+ - **Azure AD authentication** — uses your organization's existing Azure App Registration with OAuth 2.0. Credentials are never stored in the codebase.
178
+ - **Principle of least privilege** — app-only auth for read/write operations, delegated auth only when required (navigation, permissions). You control exactly which permissions are granted.
179
+ - **Device Code Flow** — delegated auth uses Microsoft's standard device code flow (same as Azure CLI, GitHub CLI). No localhost servers, no open ports, no redirect URIs needed.
180
+ - **Token security** — refresh tokens are stored locally in `~/.365center-mcp/token-cache.json` with filesystem permissions. Tokens are never transmitted to third parties.
181
+ - **Docker isolation** — runs as non-root user (`mcp`) inside the container. Token cache is mounted as a volume, not baked into the image.
182
+ - **No secrets in Docker image** — credentials are passed as environment variables at runtime, never included in the build.
183
+ - **MCP stdio transport** — communicates via stdin/stdout only. No HTTP server, no exposed ports, no network attack surface.
184
+ - **BSL license** — source code is fully auditable. Your security team can review every line before deployment.
185
+
186
+ ### Recommended deployment
187
+
188
+ For production environments:
189
+
190
+ 1. Create a dedicated Azure App Registration for 365center-mcp
191
+ 2. Grant only the permissions your workflows need
192
+ 3. Use Docker with volume mount for token persistence
193
+ 4. Store credentials in your organization's secret manager (Azure Key Vault, HashiCorp Vault, etc.)
194
+ 5. Restrict App Registration to specific SharePoint sites if possible
195
+
196
+ ## Environment Variables
197
+
198
+ | Variable | Required | Description |
199
+ |----------|----------|-------------|
200
+ | `AZURE_TENANT_ID` | Yes | Azure AD tenant ID |
201
+ | `AZURE_CLIENT_ID` | Yes | App registration client ID |
202
+ | `AZURE_CLIENT_SECRET` | Yes | App registration client secret |
203
+ | `SHAREPOINT_DOMAIN` | Yes | SharePoint domain (e.g. `contoso.sharepoint.com`) |
204
+
205
+ ## Architecture
206
+
207
+ ```
208
+ Claude Desktop / Claude Code / API
209
+
210
+ │ stdio (stdin/stdout)
211
+
212
+ 365center-mcp (MCP Server)
213
+
214
+ ├── Microsoft Graph API (v1.0)
215
+ │ └── Sites, Documents, Pages, Metadata
216
+
217
+ └── SharePoint REST API
218
+ └── Navigation, Permissions, CanvasContent1
219
+ ```
220
+
221
+ - **Graph API** uses app-only auth (Client Credentials) — no user interaction needed
222
+ - **REST API** uses delegated auth (Device Code Flow) — one-time login, then automatic token refresh
223
+
224
+ ## Docker Details
225
+
226
+ The Docker image runs as non-root user `mcp` and communicates only via stdio.
227
+
228
+ ```bash
229
+ # Build
230
+ docker build -t 365center-mcp:latest ./mcp-server
231
+
232
+ # Run standalone (for testing)
233
+ docker run -i --rm \
234
+ -e AZURE_TENANT_ID=your-tenant-id \
235
+ -e AZURE_CLIENT_ID=your-client-id \
236
+ -e AZURE_CLIENT_SECRET=your-client-secret \
237
+ -e SHAREPOINT_DOMAIN=your-domain.sharepoint.com \
238
+ -v ~/.365center-mcp:/home/mcp/.365center-mcp \
239
+ crscristi28/365center-mcp:latest
240
+ ```
241
+
242
+ The `-v` flag mounts the token cache directory so delegated auth tokens persist across container restarts. Without it, you'd need to re-authenticate every time the container starts.
243
+
244
+ ## Token Storage
245
+
246
+ Delegated auth tokens are stored in `~/.365center-mcp/token-cache.json`. This file contains:
247
+ - Access token (expires in ~1 hour, refreshed automatically)
248
+ - Refresh token (long-lived, used to get new access tokens)
249
+
250
+ For Docker, mount `~/.365center-mcp` as a volume. The token file has the same security sensitivity as your Azure credentials — protect it accordingly.
251
+
252
+ ## License
253
+
254
+ [Business Source License 1.1](LICENSE) — Free for internal use, testing, and development. Commercial use that competes with 365center-mcp requires written permission. Converts to MIT on 2030-04-08.
255
+
256
+ ## Author
257
+
258
+ Cristian Bucioacă — [cristianb.cz](https://cristianb.cz) — [info@cristianb.cz](mailto:info@cristianb.cz)
package/dist/auth.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { ClientSecretCredential } from "@azure/identity";
2
+ import { Client } from "@microsoft/microsoft-graph-client";
3
+ export declare const credential: ClientSecretCredential;
4
+ export declare const graphClient: Client;
5
+ export declare function getDelegatedToken(): Promise<string>;
6
+ export declare function getSharePointToken(): Promise<string>;
7
+ export declare function getSharePointDelegatedToken(): Promise<string>;
8
+ export declare function callSharePointRest(siteUrl: string, apiPath: string, method?: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<unknown>;
package/dist/auth.js ADDED
@@ -0,0 +1,200 @@
1
+ import { ClientSecretCredential } from "@azure/identity";
2
+ import { Client } from "@microsoft/microsoft-graph-client";
3
+ import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials/index.js";
4
+ import dotenv from "dotenv";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+ import fs from "fs";
8
+ // Load .env — try parent directory first (local dev), fallback to env vars (Docker)
9
+ const __dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
10
+ dotenv.config({ path: path.resolve(__dirname, "../../.env") });
11
+ const tenantId = process.env.AZURE_TENANT_ID;
12
+ const clientId = process.env.AZURE_CLIENT_ID;
13
+ const clientSecret = process.env.AZURE_CLIENT_SECRET;
14
+ // ============ APP-ONLY AUTH (Graph API) ============
15
+ export const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
16
+ const authProvider = new TokenCredentialAuthenticationProvider(credential, {
17
+ scopes: ["https://graph.microsoft.com/.default"],
18
+ });
19
+ export const graphClient = Client.initWithMiddleware({
20
+ authProvider,
21
+ });
22
+ // ============ DELEGATED AUTH (SharePoint REST API) ============
23
+ // Store token in user's home directory — works for Node, Docker (with volume), and plugins
24
+ const TOKEN_DIR = path.join(process.env.HOME || process.env.USERPROFILE || "/tmp", ".365center-mcp");
25
+ if (!fs.existsSync(TOKEN_DIR))
26
+ fs.mkdirSync(TOKEN_DIR, { recursive: true });
27
+ const TOKEN_CACHE_PATH = path.join(TOKEN_DIR, "token-cache.json");
28
+ const SHAREPOINT_DOMAIN = process.env.SHAREPOINT_DOMAIN || "";
29
+ const SP_SCOPES = `offline_access https://${SHAREPOINT_DOMAIN}/AllSites.FullControl`;
30
+ function loadTokenCache() {
31
+ try {
32
+ if (fs.existsSync(TOKEN_CACHE_PATH)) {
33
+ return JSON.parse(fs.readFileSync(TOKEN_CACHE_PATH, "utf-8"));
34
+ }
35
+ }
36
+ catch { }
37
+ return null;
38
+ }
39
+ function saveTokenCache(cache) {
40
+ fs.writeFileSync(TOKEN_CACHE_PATH, JSON.stringify(cache, null, 2));
41
+ }
42
+ async function refreshAccessToken(refreshToken) {
43
+ const body = new URLSearchParams({
44
+ client_id: clientId,
45
+ client_secret: clientSecret,
46
+ grant_type: "refresh_token",
47
+ refresh_token: refreshToken,
48
+ scope: SP_SCOPES,
49
+ });
50
+ const response = await fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body });
51
+ if (!response.ok) {
52
+ const err = await response.text();
53
+ throw new Error(`Token refresh failed: ${err}`);
54
+ }
55
+ const data = await response.json();
56
+ const cache = {
57
+ accessToken: data.access_token,
58
+ refreshToken: data.refresh_token || refreshToken,
59
+ expiresAt: Date.now() + data.expires_in * 1000,
60
+ };
61
+ saveTokenCache(cache);
62
+ return cache;
63
+ }
64
+ // Device code polling — runs in background after user gets instructions
65
+ let deviceCodePollingPromise = null;
66
+ function pollForDeviceCodeToken(deviceCode, interval, expiresIn) {
67
+ return new Promise(async (resolve, reject) => {
68
+ const pollInterval = (interval || 5) * 1000;
69
+ const deadline = Date.now() + expiresIn * 1000;
70
+ while (Date.now() < deadline) {
71
+ await new Promise(r => setTimeout(r, pollInterval));
72
+ const tokenResponse = await fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, {
73
+ method: "POST",
74
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
75
+ body: new URLSearchParams({
76
+ client_id: clientId,
77
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
78
+ device_code: deviceCode,
79
+ }),
80
+ });
81
+ const tokenData = await tokenResponse.json();
82
+ if (tokenData.access_token) {
83
+ const cache = {
84
+ accessToken: tokenData.access_token,
85
+ refreshToken: tokenData.refresh_token,
86
+ expiresAt: Date.now() + tokenData.expires_in * 1000,
87
+ };
88
+ saveTokenCache(cache);
89
+ deviceCodePollingPromise = null;
90
+ resolve(cache);
91
+ return;
92
+ }
93
+ if (tokenData.error === "authorization_pending")
94
+ continue;
95
+ if (tokenData.error === "slow_down") {
96
+ await new Promise(r => setTimeout(r, 5000));
97
+ continue;
98
+ }
99
+ deviceCodePollingPromise = null;
100
+ reject(new Error(`Device code auth failed: ${tokenData.error} — ${tokenData.error_description}`));
101
+ return;
102
+ }
103
+ deviceCodePollingPromise = null;
104
+ reject(new Error("Login timeout — user did not complete authentication in time"));
105
+ });
106
+ }
107
+ // Throws a user-facing error with login instructions, starts background polling
108
+ async function startDeviceCodeFlow() {
109
+ if (!SHAREPOINT_DOMAIN) {
110
+ throw new Error("SHAREPOINT_DOMAIN environment variable is required for delegated auth");
111
+ }
112
+ const codeResponse = await fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/devicecode`, {
113
+ method: "POST",
114
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
115
+ body: new URLSearchParams({
116
+ client_id: clientId,
117
+ scope: SP_SCOPES,
118
+ }),
119
+ });
120
+ if (!codeResponse.ok) {
121
+ const err = await codeResponse.text();
122
+ throw new Error(`Device code request failed: ${err}`);
123
+ }
124
+ const codeData = await codeResponse.json();
125
+ const { device_code, user_code, verification_uri, expires_in, interval, message } = codeData;
126
+ // Start polling in background
127
+ deviceCodePollingPromise = pollForDeviceCodeToken(device_code, interval, expires_in);
128
+ // Throw error with login instructions — Claude sees this and tells the user
129
+ throw new Error(`LOGIN REQUIRED: ${message}\n\n` +
130
+ `Go to: ${verification_uri}\n` +
131
+ `Enter code: ${user_code}\n\n` +
132
+ `After logging in, try your request again.`);
133
+ }
134
+ export async function getDelegatedToken() {
135
+ // 1. Check cache
136
+ const cache = loadTokenCache();
137
+ if (cache) {
138
+ if (cache.expiresAt > Date.now() + 300000) {
139
+ return cache.accessToken;
140
+ }
141
+ try {
142
+ const refreshed = await refreshAccessToken(cache.refreshToken);
143
+ return refreshed.accessToken;
144
+ }
145
+ catch {
146
+ // Refresh failed — need new login
147
+ }
148
+ }
149
+ // 2. If polling is already running, wait for it
150
+ if (deviceCodePollingPromise) {
151
+ const result = await deviceCodePollingPromise;
152
+ return result.accessToken;
153
+ }
154
+ // 3. No cache, no polling — start device code flow (throws with instructions)
155
+ await startDeviceCodeFlow();
156
+ throw new Error("unreachable"); // startDeviceCodeFlow always throws
157
+ }
158
+ // ============ SHAREPOINT REST API HELPERS ============
159
+ // App-only token for SharePoint REST API (limited — doesn't work for navigation/permissions)
160
+ export async function getSharePointToken() {
161
+ const domain = process.env.SHAREPOINT_DOMAIN;
162
+ if (!domain)
163
+ throw new Error("SHAREPOINT_DOMAIN environment variable is required");
164
+ const token = await credential.getToken(`https://${domain}/.default`);
165
+ return token.token;
166
+ }
167
+ // Delegated token for SharePoint REST API (full access — navigation, permissions, CanvasContent1)
168
+ export async function getSharePointDelegatedToken() {
169
+ // Delegated token from Graph scopes also works for SharePoint REST API
170
+ // because Sites.FullControl.All grants access to SharePoint endpoints
171
+ return getDelegatedToken();
172
+ }
173
+ export async function callSharePointRest(siteUrl, apiPath, method = "GET", body, extraHeaders) {
174
+ const token = await getSharePointDelegatedToken();
175
+ const url = `${siteUrl}${apiPath}`;
176
+ const headers = {
177
+ "Authorization": `Bearer ${token}`,
178
+ "Accept": "application/json;odata=verbose",
179
+ "Content-Type": "application/json;odata=verbose",
180
+ ...extraHeaders,
181
+ };
182
+ // SharePoint REST API uses POST + X-HTTP-Method for MERGE/PUT/PATCH
183
+ let httpMethod = method;
184
+ if (method === "MERGE" || method === "PUT" || method === "PATCH") {
185
+ headers["X-HTTP-Method"] = method;
186
+ headers["IF-MATCH"] = "*";
187
+ httpMethod = "POST";
188
+ }
189
+ const options = { method: httpMethod, headers };
190
+ if (body) {
191
+ options.body = JSON.stringify(body);
192
+ }
193
+ const response = await fetch(url, options);
194
+ if (!response.ok) {
195
+ const errorText = await response.text();
196
+ throw new Error(`SharePoint REST API error ${response.status}: ${errorText}`);
197
+ }
198
+ const text = await response.text();
199
+ return text ? JSON.parse(text) : null;
200
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};