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 +10 -8
- package/dist/index.js +28 -2
- package/dist/tools/documents.d.ts +12 -0
- package/dist/tools/documents.js +88 -8
- package/dist/tools/metadata.js +45 -20
- package/dist/tools/sites-rest.d.ts +5 -0
- package/dist/tools/sites-rest.js +21 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 365center-mcp
|
|
2
2
|
|
|
3
|
-
**MCP server for Microsoft 365 / SharePoint —
|
|
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
|
[](https://github.com/Crscristi28/365center-mcp/blob/main/LICENSE)
|
|
10
10
|
[](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 **
|
|
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
|
-
**
|
|
59
|
+
**35 tools** across 7 categories. All tools use Microsoft Graph API or SharePoint REST API — no middlemen.
|
|
60
60
|
|
|
61
|
-
### Sites (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
+
}[]>;
|
package/dist/tools/documents.js
CHANGED
|
@@ -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
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
.
|
|
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:
|
|
39
|
-
name:
|
|
40
|
-
url:
|
|
41
|
-
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
|
+
}
|
package/dist/tools/metadata.js
CHANGED
|
@@ -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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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.
|
|
59
|
-
name:
|
|
60
|
-
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(
|
|
110
|
+
.patch(patchFields);
|
|
86
111
|
return result;
|
|
87
112
|
}
|
|
88
113
|
export async function getDocumentMetadata(siteId, listId, itemId, driveId) {
|
|
@@ -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.
|
|
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",
|