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 +25 -0
- package/README.md +258 -0
- package/dist/auth.d.ts +8 -0
- package/dist/auth.js +200 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +269 -0
- package/dist/tools/documents.d.ts +19 -0
- package/dist/tools/documents.js +84 -0
- package/dist/tools/metadata.d.ts +14 -0
- package/dist/tools/metadata.js +94 -0
- package/dist/tools/navigation.d.ts +10 -0
- package/dist/tools/navigation.js +27 -0
- package/dist/tools/pages-rest.d.ts +16 -0
- package/dist/tools/pages-rest.js +47 -0
- package/dist/tools/pages.d.ts +32 -0
- package/dist/tools/pages.js +152 -0
- package/dist/tools/permissions.d.ts +13 -0
- package/dist/tools/permissions.js +37 -0
- package/dist/tools/sites.d.ts +13 -0
- package/dist/tools/sites.js +40 -0
- package/package.json +51 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED