@habibakhaledm/sharepoint-document-upload-mcp 1.1.3 → 1.1.4
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/dist/src/constants.d.ts +95 -0
- package/dist/src/constants.js +102 -0
- package/dist/src/document-upload.d.ts +4 -0
- package/dist/src/document-upload.js +61 -0
- package/dist/src/graph-client.d.ts +20 -0
- package/dist/src/graph-client.js +53 -0
- package/dist/src/server.d.ts +2 -0
- package/dist/src/server.js +61 -0
- package/dist/src/sharepoint.d.ts +29 -0
- package/dist/src/sharepoint.js +577 -0
- package/package.json +5 -4
- package/src/server.ts +1 -2
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** Central configuration literals (no secrets). */
|
|
2
|
+
export declare const GRAPH_V1_BASE_URL: "https://graph.microsoft.com/v1.0";
|
|
3
|
+
export declare const GRAPH_BETA_BASE_URL: "https://graph.microsoft.com/beta";
|
|
4
|
+
export declare const MICROSOFT_AUTH_MODE: "interactive";
|
|
5
|
+
export declare const MICROSOFT_REDIRECT_URI: "http://localhost";
|
|
6
|
+
/** Delegated scopes requested for interactive SharePoint upload. */
|
|
7
|
+
export declare const GRAPH_DELEGATED_SCOPES: readonly ["https://graph.microsoft.com/Sites.Read.All", "https://graph.microsoft.com/Files.ReadWrite.All"];
|
|
8
|
+
export declare const ENTRA_DEFAULT_REDIRECT_URI: "https://login.microsoftonline.com/common/oauth2/nativeclient";
|
|
9
|
+
/** Used when MICROSOFT_TENANT_ID is omitted. */
|
|
10
|
+
export declare const ENTRA_TENANT_FALLBACK_ORGANIZATIONS: "organizations";
|
|
11
|
+
/**
|
|
12
|
+
* MSAL disk cache folder name under the OS store (e.g. Windows:
|
|
13
|
+
* %LocalAppData%\\.IdentityService\\<name>). Override with MICROSOFT_TOKEN_CACHE_NAME.
|
|
14
|
+
* Set MICROSOFT_DISABLE_TOKEN_CACHE=true to skip persistence (browser every run).
|
|
15
|
+
* If cache init fails without OS encryption, set MICROSOFT_TOKEN_CACHE_UNSAFE_UNENCRYPTED=true.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MICROSOFT_TOKEN_CACHE_DEFAULT_NAME: "document-upload-mcp-graph";
|
|
18
|
+
/**
|
|
19
|
+
* Account binding file for silent auth (Azure Identity needs this in addition to MSAL token cache).
|
|
20
|
+
* Default directory: ~/.document-upload-mcp (override MICROSOFT_AUTH_RECORD_DIR).
|
|
21
|
+
* Delete `auth-record-*.json` there to force sign-in again.
|
|
22
|
+
*/
|
|
23
|
+
export declare const MICROSOFT_AUTH_RECORD_BASE_DIR: ".document-upload-mcp";
|
|
24
|
+
export declare const SHAREPOINT_DEFAULT_DOCUMENT_LIBRARY_NAME: "Documents";
|
|
25
|
+
export declare const HTTP_CONTENT_TYPE_JSON: "application/json";
|
|
26
|
+
export declare const HTTP_CONTENT_TYPE_OCTET_STREAM: "application/octet-stream";
|
|
27
|
+
export declare const MCP_SERVER_NAME: "sharepoint-document-upload-mcp";
|
|
28
|
+
export declare const MCP_SERVER_VERSION: "1.0.0";
|
|
29
|
+
export declare const MCP_SERVER_DESCRIPTION: "A MCP server for uploading document";
|
|
30
|
+
export declare const ALLOWED_UPLOAD_EXTENSIONS: readonly [".txt", ".md", ".pdf", ".docx"];
|
|
31
|
+
export declare const LOCAL_UPLOAD_DIR: "uploads";
|
|
32
|
+
/** Prefix length from SHA-256 hex used in SharePoint upload filenames. */
|
|
33
|
+
export declare const SHAREPOINT_FILENAME_HASH_PREFIX_LENGTH = 12;
|
|
34
|
+
/** Used with `crypto.createHash` for document deduplication and filenames. */
|
|
35
|
+
export declare const DOCUMENT_HASH_ALGORITHM: "sha256";
|
|
36
|
+
export declare const TOOL_NAME_HELLO_WORLD: "hello_world";
|
|
37
|
+
export declare const TOOL_NAME_UPLOAD_DOCUMENT: "upload_document";
|
|
38
|
+
export declare const TOOL_HELLO_WORLD_TITLE: "Hello World";
|
|
39
|
+
export declare const TOOL_HELLO_WORLD_DESCRIPTION: "This tool will return Hello World";
|
|
40
|
+
export declare const TOOL_HELLO_WORLD_RESPONSE_TEXT: "Hello World";
|
|
41
|
+
export declare const TOOL_UPLOAD_DOCUMENT_TITLE: "Upload a document";
|
|
42
|
+
export declare const TOOL_UPLOAD_DOCUMENT_DESCRIPTION: "Upload a document locally and to SharePoint when configured. Uses interactive Microsoft sign-in with Graph scopes Sites.Read.All and Files.ReadWrite.All only \u2014 remove other API permissions on the Entra app for SharePoint-only consent. After the first sign-in, account metadata and tokens are stored on disk (see ~/.document-upload-mcp and OS .IdentityService cache) so later runs usually skip the browser. Set MICROSOFT_DISABLE_TOKEN_CACHE=true only to disable the MSAL file cache.";
|
|
43
|
+
/** Zod field descriptions for `upload_document` input schema */
|
|
44
|
+
export declare const SCHEMA_DESC_DOCUMENT_FILENAME: "Original filename including extension (e.g. report.pdf)";
|
|
45
|
+
export declare const SCHEMA_DESC_DOCUMENT_CONTENT: "File body as UTF-8 text or base64-encoded bytes";
|
|
46
|
+
export declare const SCHEMA_DESC_MICROSOFT_TENANT_ID: "Entra tenant ID (optional; env MICROSOFT_TENANT_ID; omit for organizations / multi-tenant)";
|
|
47
|
+
export declare const SCHEMA_DESC_MICROSOFT_CLIENT_ID: "App registration client ID (required for interactive browser sign-in; env MICROSOFT_CLIENT_ID)";
|
|
48
|
+
export declare const SCHEMA_DESC_SHAREPOINT_SITE_HOST: "SharePoint hostname, e.g. contoso.sharepoint.com (or set SHAREPOINT_SITE_HOST env)";
|
|
49
|
+
export declare const SCHEMA_DESC_SHAREPOINT_SITE_PATH: "Server-relative site path (e.g. /sites/Team). Use empty string for the root site";
|
|
50
|
+
export declare const SCHEMA_DESC_SHAREPOINT_UPLOAD_BASE_PATH: "Folder under the document library (e.g. Incoming/2026). Omit or empty for library root";
|
|
51
|
+
export declare const ERR_UNSUPPORTED_FILE_TYPE: "Unsupported file type";
|
|
52
|
+
export declare const MSG_UPLOAD_SUCCESS_TEMPLATE: (sharePointNote: string) => string;
|
|
53
|
+
/** Appended when Graph returns a browser URL for the uploaded item. */
|
|
54
|
+
export declare const MSG_SHAREPOINT_DOC_URL_LINE: (url: string) => `
|
|
55
|
+
|
|
56
|
+
SharePoint document URL:
|
|
57
|
+
${string}`;
|
|
58
|
+
/** Opaque Microsoft Graph `driveItem` id (for `/drives/.../items/{id}`). */
|
|
59
|
+
export declare const MSG_SHAREPOINT_DRIVE_ITEM_ID_LINE: (id: string) => `
|
|
60
|
+
|
|
61
|
+
Microsoft Graph drive item id:
|
|
62
|
+
${string}`;
|
|
63
|
+
/** SharePoint list row id (often what people mean by "item id" in libraries). */
|
|
64
|
+
export declare const MSG_SHAREPOINT_LIST_ITEM_ID_LINE: (id: string) => `
|
|
65
|
+
|
|
66
|
+
SharePoint list item id:
|
|
67
|
+
${string}`;
|
|
68
|
+
export declare const MSG_SHAREPOINT_LIST_ITEM_UNIQUE_ID_LINE: (id: string) => `
|
|
69
|
+
|
|
70
|
+
SharePoint list item unique id:
|
|
71
|
+
${string}`;
|
|
72
|
+
export declare const SP_RESULT_NOTE_COMPLETED: "\n\nSharePoint upload completed (no document URL or ids in API response).";
|
|
73
|
+
export declare const ERR_INTERACTIVE_NO_GRAPH_TOKEN: "Interactive sign-in did not return a Microsoft Graph token.";
|
|
74
|
+
export declare const ERR_CONSENT_DECLINED_HINT: " Grant only delegated Sites.Read.All and Files.ReadWrite.All on the app registration, then accept consent.";
|
|
75
|
+
export declare const AADSTS65004_SNIPPET: "AADSTS65004";
|
|
76
|
+
export declare const CONSENT_DECLINED_SNIPPET: "declined to consent";
|
|
77
|
+
export declare const ERR_SHAREPOINT_NO_DRIVES: "No drives found on SharePoint site";
|
|
78
|
+
export declare const ERR_MICROSOFT_CLIENT_ID_REQUIRED: (redirectUri: string) => string;
|
|
79
|
+
export declare const ERR_GRAPH_FOLDER_CHECK: (pathSoFar: string, status: number, body: string) => string;
|
|
80
|
+
export declare const ERR_GRAPH_CREATE_FOLDER: (segment: string, parentPath: string, status: number, body: string) => string;
|
|
81
|
+
export declare const MICROSOFT_GRAPH_CONFLICT_BEHAVIOR_FAIL: "fail";
|
|
82
|
+
/** Set to `1` to log why `*-my.sharepoint.com/shared` URL construction was skipped. */
|
|
83
|
+
export declare const DEBUG_SHAREPOINT_MY_URL_ENV: "SHAREPOINT_DEBUG_MY_URL";
|
|
84
|
+
export declare const ERR_GRAPH_GET: (graphPath: string, status: number, body: string) => string;
|
|
85
|
+
export declare const ERR_GRAPH_PUT: (graphPath: string, status: number, body: string) => string;
|
|
86
|
+
export declare const MCP_UPLOAD_TEST_CLIENT_NAME: "upload-test";
|
|
87
|
+
export declare const MCP_UPLOAD_TEST_CLIENT_VERSION: "1.0.0";
|
|
88
|
+
export declare const MCP_CALL_TIMEOUT_ENV: "MCP_CALL_TIMEOUT_MS";
|
|
89
|
+
export declare const MCP_CALL_TIMEOUT_DEFAULT_MS = 900000;
|
|
90
|
+
/** Default fixture under `fixtures/`; override with env MCP_UPLOAD_TEST_FIXTURE. */
|
|
91
|
+
export declare const FIXTURE_UPLOAD_TEST_FILENAME: "mcp-random-test.md";
|
|
92
|
+
export declare const MCP_UPLOAD_TEST_FIXTURE_ENV: "MCP_UPLOAD_TEST_FIXTURE";
|
|
93
|
+
/** Project-relative paths (repo root). */
|
|
94
|
+
export declare const PROJECT_REL_TSX_CLI: "node_modules/tsx/dist/cli.mjs";
|
|
95
|
+
export declare const PROJECT_REL_SERVER_ENTRY: "src/server.ts";
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Central configuration literals (no secrets). */
|
|
2
|
+
/* Microsoft Graph */
|
|
3
|
+
export const GRAPH_V1_BASE_URL = "https://graph.microsoft.com/v1.0";
|
|
4
|
+
export const GRAPH_BETA_BASE_URL = "https://graph.microsoft.com/beta";
|
|
5
|
+
export const MICROSOFT_AUTH_MODE = "interactive";
|
|
6
|
+
export const MICROSOFT_REDIRECT_URI = "http://localhost";
|
|
7
|
+
/** Delegated scopes requested for interactive SharePoint upload. */
|
|
8
|
+
export const GRAPH_DELEGATED_SCOPES = [
|
|
9
|
+
"https://graph.microsoft.com/Sites.Read.All",
|
|
10
|
+
"https://graph.microsoft.com/Files.ReadWrite.All",
|
|
11
|
+
];
|
|
12
|
+
/* Microsoft Entra (interactive public client) */
|
|
13
|
+
export const ENTRA_DEFAULT_REDIRECT_URI = "https://login.microsoftonline.com/common/oauth2/nativeclient";
|
|
14
|
+
/** Used when MICROSOFT_TENANT_ID is omitted. */
|
|
15
|
+
export const ENTRA_TENANT_FALLBACK_ORGANIZATIONS = "organizations";
|
|
16
|
+
/**
|
|
17
|
+
* MSAL disk cache folder name under the OS store (e.g. Windows:
|
|
18
|
+
* %LocalAppData%\\.IdentityService\\<name>). Override with MICROSOFT_TOKEN_CACHE_NAME.
|
|
19
|
+
* Set MICROSOFT_DISABLE_TOKEN_CACHE=true to skip persistence (browser every run).
|
|
20
|
+
* If cache init fails without OS encryption, set MICROSOFT_TOKEN_CACHE_UNSAFE_UNENCRYPTED=true.
|
|
21
|
+
*/
|
|
22
|
+
export const MICROSOFT_TOKEN_CACHE_DEFAULT_NAME = "document-upload-mcp-graph";
|
|
23
|
+
/**
|
|
24
|
+
* Account binding file for silent auth (Azure Identity needs this in addition to MSAL token cache).
|
|
25
|
+
* Default directory: ~/.document-upload-mcp (override MICROSOFT_AUTH_RECORD_DIR).
|
|
26
|
+
* Delete `auth-record-*.json` there to force sign-in again.
|
|
27
|
+
*/
|
|
28
|
+
export const MICROSOFT_AUTH_RECORD_BASE_DIR = ".document-upload-mcp";
|
|
29
|
+
/* SharePoint */
|
|
30
|
+
export const SHAREPOINT_DEFAULT_DOCUMENT_LIBRARY_NAME = "Documents";
|
|
31
|
+
/* HTTP (Graph requests) */
|
|
32
|
+
export const HTTP_CONTENT_TYPE_JSON = "application/json";
|
|
33
|
+
export const HTTP_CONTENT_TYPE_OCTET_STREAM = "application/octet-stream";
|
|
34
|
+
/* MCP server metadata */
|
|
35
|
+
export const MCP_SERVER_NAME = "sharepoint-document-upload-mcp";
|
|
36
|
+
export const MCP_SERVER_VERSION = "1.0.0";
|
|
37
|
+
export const MCP_SERVER_DESCRIPTION = "A MCP server for uploading document";
|
|
38
|
+
/* Local document handling */
|
|
39
|
+
export const ALLOWED_UPLOAD_EXTENSIONS = [
|
|
40
|
+
".txt",
|
|
41
|
+
".md",
|
|
42
|
+
".pdf",
|
|
43
|
+
".docx",
|
|
44
|
+
];
|
|
45
|
+
export const LOCAL_UPLOAD_DIR = "uploads";
|
|
46
|
+
/** Prefix length from SHA-256 hex used in SharePoint upload filenames. */
|
|
47
|
+
export const SHAREPOINT_FILENAME_HASH_PREFIX_LENGTH = 12;
|
|
48
|
+
/** Used with `crypto.createHash` for document deduplication and filenames. */
|
|
49
|
+
export const DOCUMENT_HASH_ALGORITHM = "sha256";
|
|
50
|
+
/* MCP tools — names, titles, descriptions, responses */
|
|
51
|
+
export const TOOL_NAME_HELLO_WORLD = "hello_world";
|
|
52
|
+
export const TOOL_NAME_UPLOAD_DOCUMENT = "upload_document";
|
|
53
|
+
export const TOOL_HELLO_WORLD_TITLE = "Hello World";
|
|
54
|
+
export const TOOL_HELLO_WORLD_DESCRIPTION = "This tool will return Hello World";
|
|
55
|
+
export const TOOL_HELLO_WORLD_RESPONSE_TEXT = "Hello World";
|
|
56
|
+
export const TOOL_UPLOAD_DOCUMENT_TITLE = "Upload a document";
|
|
57
|
+
export const TOOL_UPLOAD_DOCUMENT_DESCRIPTION = "Upload a document locally and to SharePoint when configured. Uses interactive Microsoft sign-in with Graph scopes Sites.Read.All and Files.ReadWrite.All only — remove other API permissions on the Entra app for SharePoint-only consent. After the first sign-in, account metadata and tokens are stored on disk (see ~/.document-upload-mcp and OS .IdentityService cache) so later runs usually skip the browser. Set MICROSOFT_DISABLE_TOKEN_CACHE=true only to disable the MSAL file cache.";
|
|
58
|
+
/** Zod field descriptions for `upload_document` input schema */
|
|
59
|
+
export const SCHEMA_DESC_DOCUMENT_FILENAME = "Original filename including extension (e.g. report.pdf)";
|
|
60
|
+
export const SCHEMA_DESC_DOCUMENT_CONTENT = "File body as UTF-8 text or base64-encoded bytes";
|
|
61
|
+
export const SCHEMA_DESC_MICROSOFT_TENANT_ID = "Entra tenant ID (optional; env MICROSOFT_TENANT_ID; omit for organizations / multi-tenant)";
|
|
62
|
+
export const SCHEMA_DESC_MICROSOFT_CLIENT_ID = "App registration client ID (required for interactive browser sign-in; env MICROSOFT_CLIENT_ID)";
|
|
63
|
+
export const SCHEMA_DESC_SHAREPOINT_SITE_HOST = "SharePoint hostname, e.g. contoso.sharepoint.com (or set SHAREPOINT_SITE_HOST env)";
|
|
64
|
+
export const SCHEMA_DESC_SHAREPOINT_SITE_PATH = "Server-relative site path (e.g. /sites/Team). Use empty string for the root site";
|
|
65
|
+
export const SCHEMA_DESC_SHAREPOINT_UPLOAD_BASE_PATH = "Folder under the document library (e.g. Incoming/2026). Omit or empty for library root";
|
|
66
|
+
/* Server messages */
|
|
67
|
+
export const ERR_UNSUPPORTED_FILE_TYPE = "Unsupported file type";
|
|
68
|
+
export const MSG_UPLOAD_SUCCESS_TEMPLATE = (sharePointNote) => `Upload successful.${sharePointNote}`;
|
|
69
|
+
/** Appended when Graph returns a browser URL for the uploaded item. */
|
|
70
|
+
export const MSG_SHAREPOINT_DOC_URL_LINE = (url) => `\n\nSharePoint document URL:\n${url}`;
|
|
71
|
+
/** Opaque Microsoft Graph `driveItem` id (for `/drives/.../items/{id}`). */
|
|
72
|
+
export const MSG_SHAREPOINT_DRIVE_ITEM_ID_LINE = (id) => `\n\nMicrosoft Graph drive item id:\n${id}`;
|
|
73
|
+
/** SharePoint list row id (often what people mean by "item id" in libraries). */
|
|
74
|
+
export const MSG_SHAREPOINT_LIST_ITEM_ID_LINE = (id) => `\n\nSharePoint list item id:\n${id}`;
|
|
75
|
+
export const MSG_SHAREPOINT_LIST_ITEM_UNIQUE_ID_LINE = (id) => `\n\nSharePoint list item unique id:\n${id}`;
|
|
76
|
+
export const SP_RESULT_NOTE_COMPLETED = "\n\nSharePoint upload completed (no document URL or ids in API response).";
|
|
77
|
+
/* SharePoint / Graph runtime errors (user-facing) */
|
|
78
|
+
export const ERR_INTERACTIVE_NO_GRAPH_TOKEN = "Interactive sign-in did not return a Microsoft Graph token.";
|
|
79
|
+
export const ERR_CONSENT_DECLINED_HINT = " Grant only delegated Sites.Read.All and Files.ReadWrite.All on the app registration, then accept consent.";
|
|
80
|
+
export const AADSTS65004_SNIPPET = "AADSTS65004";
|
|
81
|
+
export const CONSENT_DECLINED_SNIPPET = "declined to consent";
|
|
82
|
+
export const ERR_SHAREPOINT_NO_DRIVES = "No drives found on SharePoint site";
|
|
83
|
+
export const ERR_MICROSOFT_CLIENT_ID_REQUIRED = (redirectUri) => `MICROSOFT_CLIENT_ID is required. Use a public Entra client with redirect "${redirectUri}" and delegated Sites.Read.All + Files.ReadWrite.All.`;
|
|
84
|
+
export const ERR_GRAPH_FOLDER_CHECK = (pathSoFar, status, body) => `Graph folder check ${pathSoFar}: ${status} ${body}`;
|
|
85
|
+
export const ERR_GRAPH_CREATE_FOLDER = (segment, parentPath, status, body) => `Create folder "${segment}" under "${parentPath || "root"}": ${status} ${body}`;
|
|
86
|
+
export const MICROSOFT_GRAPH_CONFLICT_BEHAVIOR_FAIL = "fail";
|
|
87
|
+
/** Set to `1` to log why `*-my.sharepoint.com/shared` URL construction was skipped. */
|
|
88
|
+
export const DEBUG_SHAREPOINT_MY_URL_ENV = "SHAREPOINT_DEBUG_MY_URL";
|
|
89
|
+
/* Graph client error prefixes */
|
|
90
|
+
export const ERR_GRAPH_GET = (graphPath, status, body) => `Graph GET ${graphPath}: ${status} ${body}`;
|
|
91
|
+
export const ERR_GRAPH_PUT = (graphPath, status, body) => `Graph PUT ${graphPath}: ${status} ${body}`;
|
|
92
|
+
/* Integration test script (run-mcp-upload-test) */
|
|
93
|
+
export const MCP_UPLOAD_TEST_CLIENT_NAME = "upload-test";
|
|
94
|
+
export const MCP_UPLOAD_TEST_CLIENT_VERSION = "1.0.0";
|
|
95
|
+
export const MCP_CALL_TIMEOUT_ENV = "MCP_CALL_TIMEOUT_MS";
|
|
96
|
+
export const MCP_CALL_TIMEOUT_DEFAULT_MS = 900_000;
|
|
97
|
+
/** Default fixture under `fixtures/`; override with env MCP_UPLOAD_TEST_FIXTURE. */
|
|
98
|
+
export const FIXTURE_UPLOAD_TEST_FILENAME = "mcp-random-test.md";
|
|
99
|
+
export const MCP_UPLOAD_TEST_FIXTURE_ENV = "MCP_UPLOAD_TEST_FIXTURE";
|
|
100
|
+
/** Project-relative paths (repo root). */
|
|
101
|
+
export const PROJECT_REL_TSX_CLI = "node_modules/tsx/dist/cli.mjs";
|
|
102
|
+
export const PROJECT_REL_SERVER_ENTRY = "src/server.ts";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ALLOWED_UPLOAD_EXTENSIONS, DOCUMENT_HASH_ALGORITHM, ERR_UNSUPPORTED_FILE_TYPE, LOCAL_UPLOAD_DIR, MSG_SHAREPOINT_DOC_URL_LINE, MSG_SHAREPOINT_DRIVE_ITEM_ID_LINE, MSG_SHAREPOINT_LIST_ITEM_ID_LINE, MSG_SHAREPOINT_LIST_ITEM_UNIQUE_ID_LINE, SHAREPOINT_FILENAME_HASH_PREFIX_LENGTH, SP_RESULT_NOTE_COMPLETED, } from "./constants.js";
|
|
2
|
+
import { uploadBufferToSharePointIfConfigured } from "./sharepoint.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
export async function uploadDocument(document, MICROSOFT_TENANT_ID, MICROSOFT_CLIENT_ID, SHAREPOINT_SITE_HOST, SHAREPOINT_SITE_PATH, SHAREPOINT_UPLOAD_BASE_PATH) {
|
|
7
|
+
const { filename, content } = document;
|
|
8
|
+
const ext = path.extname(filename).toLowerCase();
|
|
9
|
+
if (!ALLOWED_UPLOAD_EXTENSIONS.includes(ext)) {
|
|
10
|
+
throw new Error(ERR_UNSUPPORTED_FILE_TYPE);
|
|
11
|
+
}
|
|
12
|
+
const uploadDir = path.resolve(LOCAL_UPLOAD_DIR);
|
|
13
|
+
await fs.mkdir(uploadDir, { recursive: true });
|
|
14
|
+
const filePath = path.join(uploadDir, filename);
|
|
15
|
+
const buffer = isBase64(content)
|
|
16
|
+
? Buffer.from(content, "base64")
|
|
17
|
+
: Buffer.from(content, "utf-8");
|
|
18
|
+
await fs.writeFile(filePath, buffer);
|
|
19
|
+
const hash = crypto.createHash(DOCUMENT_HASH_ALGORITHM).update(buffer).digest("hex");
|
|
20
|
+
const sp = await uploadBufferToSharePointIfConfigured(buffer, `${hash.slice(0, SHAREPOINT_FILENAME_HASH_PREFIX_LENGTH)}_${filename}`, {
|
|
21
|
+
MICROSOFT_TENANT_ID,
|
|
22
|
+
MICROSOFT_CLIENT_ID,
|
|
23
|
+
SHAREPOINT_SITE_HOST,
|
|
24
|
+
SHAREPOINT_SITE_PATH,
|
|
25
|
+
SHAREPOINT_UPLOAD_BASE_PATH,
|
|
26
|
+
});
|
|
27
|
+
const spNote = formatSharePointResultNote(sp);
|
|
28
|
+
return spNote;
|
|
29
|
+
}
|
|
30
|
+
function formatSharePointResultNote(sp) {
|
|
31
|
+
if (sp.skipped) {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
const parts = [];
|
|
35
|
+
if (sp.webUrl) {
|
|
36
|
+
parts.push(MSG_SHAREPOINT_DOC_URL_LINE(sp.webUrl));
|
|
37
|
+
}
|
|
38
|
+
if (sp.driveItemId) {
|
|
39
|
+
parts.push(MSG_SHAREPOINT_DRIVE_ITEM_ID_LINE(sp.driveItemId));
|
|
40
|
+
}
|
|
41
|
+
const listItemId = sp.sharepointIds?.listItemId;
|
|
42
|
+
if (listItemId) {
|
|
43
|
+
parts.push(MSG_SHAREPOINT_LIST_ITEM_ID_LINE(listItemId));
|
|
44
|
+
}
|
|
45
|
+
const uniqueId = sp.sharepointIds?.listItemUniqueId;
|
|
46
|
+
if (uniqueId) {
|
|
47
|
+
parts.push(MSG_SHAREPOINT_LIST_ITEM_UNIQUE_ID_LINE(uniqueId));
|
|
48
|
+
}
|
|
49
|
+
if (parts.length > 0) {
|
|
50
|
+
return parts.join("");
|
|
51
|
+
}
|
|
52
|
+
return SP_RESULT_NOTE_COMPLETED;
|
|
53
|
+
}
|
|
54
|
+
function isBase64(str) {
|
|
55
|
+
try {
|
|
56
|
+
return Buffer.from(str, "base64").toString("base64") === str;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Minimal Microsoft Graph v1 HTTP helpers (Bearer token). */
|
|
2
|
+
export declare const GRAPH_V1_ROOT: "https://graph.microsoft.com/v1.0";
|
|
3
|
+
export declare const GRAPH_BETA_ROOT: "https://graph.microsoft.com/beta";
|
|
4
|
+
/** Subset of DriveItem fields used after upload / metadata reads. */
|
|
5
|
+
export type GraphDriveItemSummary = {
|
|
6
|
+
id?: string;
|
|
7
|
+
webUrl?: string;
|
|
8
|
+
sharepointIds?: {
|
|
9
|
+
listId?: string;
|
|
10
|
+
listItemId?: string;
|
|
11
|
+
listItemUniqueId?: string;
|
|
12
|
+
siteId?: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
/** Drive item path: encode each path segment (handles `/`, `&`, spaces). */
|
|
16
|
+
export declare function encodeDriveRelativePath(relativePath: string): string;
|
|
17
|
+
export declare function graphRequest(token: string, graphPath: string, init?: RequestInit): Promise<Response>;
|
|
18
|
+
export declare function graphGetJson<T>(token: string, graphPath: string): Promise<T>;
|
|
19
|
+
export declare function graphBetaGetJson<T>(token: string, graphPath: string): Promise<T>;
|
|
20
|
+
export declare function graphPutStream(token: string, graphPath: string, body: Buffer): Promise<GraphDriveItemSummary>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Minimal Microsoft Graph v1 HTTP helpers (Bearer token). */
|
|
2
|
+
import { ERR_GRAPH_GET, ERR_GRAPH_PUT, GRAPH_BETA_BASE_URL, GRAPH_V1_BASE_URL, HTTP_CONTENT_TYPE_OCTET_STREAM, } from "./constants.js";
|
|
3
|
+
export const GRAPH_V1_ROOT = GRAPH_V1_BASE_URL;
|
|
4
|
+
export const GRAPH_BETA_ROOT = GRAPH_BETA_BASE_URL;
|
|
5
|
+
/** Drive item path: encode each path segment (handles `/`, `&`, spaces). */
|
|
6
|
+
export function encodeDriveRelativePath(relativePath) {
|
|
7
|
+
return relativePath
|
|
8
|
+
.split("/")
|
|
9
|
+
.filter(Boolean)
|
|
10
|
+
.map((s) => encodeURIComponent(s))
|
|
11
|
+
.join("/");
|
|
12
|
+
}
|
|
13
|
+
export async function graphRequest(token, graphPath, init) {
|
|
14
|
+
const headers = new Headers(init?.headers ?? undefined);
|
|
15
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
16
|
+
return fetch(`${GRAPH_V1_ROOT}${graphPath}`, { ...init, headers });
|
|
17
|
+
}
|
|
18
|
+
export async function graphGetJson(token, graphPath) {
|
|
19
|
+
const res = await graphRequest(token, graphPath);
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
throw new Error(ERR_GRAPH_GET(graphPath, res.status, await res.text()));
|
|
22
|
+
}
|
|
23
|
+
return res.json();
|
|
24
|
+
}
|
|
25
|
+
export async function graphBetaGetJson(token, graphPath) {
|
|
26
|
+
const headers = new Headers();
|
|
27
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
28
|
+
const res = await fetch(`${GRAPH_BETA_ROOT}${graphPath}`, { headers });
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
throw new Error(ERR_GRAPH_GET(`[beta]${graphPath}`, res.status, await res.text()));
|
|
31
|
+
}
|
|
32
|
+
return res.json();
|
|
33
|
+
}
|
|
34
|
+
export async function graphPutStream(token, graphPath, body) {
|
|
35
|
+
const res = await graphRequest(token, graphPath, {
|
|
36
|
+
method: "PUT",
|
|
37
|
+
headers: { "Content-Type": HTTP_CONTENT_TYPE_OCTET_STREAM },
|
|
38
|
+
body: new Uint8Array(body),
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
throw new Error(ERR_GRAPH_PUT(graphPath, res.status, await res.text()));
|
|
42
|
+
}
|
|
43
|
+
const raw = await res.text();
|
|
44
|
+
if (!raw.trim()) {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { MCP_SERVER_DESCRIPTION, MCP_SERVER_NAME, MCP_SERVER_VERSION, MSG_UPLOAD_SUCCESS_TEMPLATE, SCHEMA_DESC_DOCUMENT_CONTENT, SCHEMA_DESC_DOCUMENT_FILENAME, SCHEMA_DESC_MICROSOFT_CLIENT_ID, SCHEMA_DESC_MICROSOFT_TENANT_ID, SCHEMA_DESC_SHAREPOINT_SITE_HOST, SCHEMA_DESC_SHAREPOINT_SITE_PATH, SCHEMA_DESC_SHAREPOINT_UPLOAD_BASE_PATH, TOOL_HELLO_WORLD_RESPONSE_TEXT, TOOL_HELLO_WORLD_TITLE, TOOL_NAME_HELLO_WORLD, TOOL_NAME_UPLOAD_DOCUMENT, TOOL_UPLOAD_DOCUMENT_DESCRIPTION, TOOL_UPLOAD_DOCUMENT_TITLE, TOOL_HELLO_WORLD_DESCRIPTION } from "./constants.js";
|
|
6
|
+
import { uploadDocument } from "./document-upload.js";
|
|
7
|
+
const server = new McpServer({
|
|
8
|
+
name: MCP_SERVER_NAME,
|
|
9
|
+
version: MCP_SERVER_VERSION,
|
|
10
|
+
description: MCP_SERVER_DESCRIPTION,
|
|
11
|
+
});
|
|
12
|
+
server.registerTool(TOOL_NAME_HELLO_WORLD, {
|
|
13
|
+
title: TOOL_HELLO_WORLD_TITLE,
|
|
14
|
+
description: TOOL_HELLO_WORLD_DESCRIPTION,
|
|
15
|
+
inputSchema: {}
|
|
16
|
+
}, async () => {
|
|
17
|
+
return { content: [
|
|
18
|
+
{
|
|
19
|
+
type: "text",
|
|
20
|
+
text: TOOL_HELLO_WORLD_RESPONSE_TEXT,
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
server.registerTool(TOOL_NAME_UPLOAD_DOCUMENT, {
|
|
26
|
+
title: TOOL_UPLOAD_DOCUMENT_TITLE,
|
|
27
|
+
description: TOOL_UPLOAD_DOCUMENT_DESCRIPTION,
|
|
28
|
+
inputSchema: z.object({
|
|
29
|
+
document: z.object({
|
|
30
|
+
filename: z.string().describe(SCHEMA_DESC_DOCUMENT_FILENAME),
|
|
31
|
+
content: z.string().describe(SCHEMA_DESC_DOCUMENT_CONTENT),
|
|
32
|
+
}),
|
|
33
|
+
MICROSOFT_TENANT_ID: z
|
|
34
|
+
.string()
|
|
35
|
+
.describe(SCHEMA_DESC_MICROSOFT_TENANT_ID),
|
|
36
|
+
MICROSOFT_CLIENT_ID: z
|
|
37
|
+
.string()
|
|
38
|
+
.describe(SCHEMA_DESC_MICROSOFT_CLIENT_ID),
|
|
39
|
+
SHAREPOINT_SITE_HOST: z
|
|
40
|
+
.string()
|
|
41
|
+
.describe(SCHEMA_DESC_SHAREPOINT_SITE_HOST),
|
|
42
|
+
SHAREPOINT_SITE_PATH: z
|
|
43
|
+
.string()
|
|
44
|
+
.describe(SCHEMA_DESC_SHAREPOINT_SITE_PATH),
|
|
45
|
+
SHAREPOINT_UPLOAD_BASE_PATH: z
|
|
46
|
+
.string()
|
|
47
|
+
.describe(SCHEMA_DESC_SHAREPOINT_UPLOAD_BASE_PATH),
|
|
48
|
+
}),
|
|
49
|
+
}, async ({ document, MICROSOFT_TENANT_ID, MICROSOFT_CLIENT_ID, SHAREPOINT_SITE_HOST, SHAREPOINT_SITE_PATH, SHAREPOINT_UPLOAD_BASE_PATH, }) => {
|
|
50
|
+
const spNote = await uploadDocument(document, MICROSOFT_TENANT_ID, MICROSOFT_CLIENT_ID, SHAREPOINT_SITE_HOST, SHAREPOINT_SITE_PATH, SHAREPOINT_UPLOAD_BASE_PATH);
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: MSG_UPLOAD_SUCCESS_TEMPLATE(spNote),
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
const transport = new StdioServerTransport();
|
|
61
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SharePoint upload via Microsoft Graph + interactive browser sign-in.
|
|
3
|
+
* See `constants.ts` for Graph scopes, redirect URI, and defaults.
|
|
4
|
+
*/
|
|
5
|
+
import { type GraphDriveItemSummary } from "./graph-client.js";
|
|
6
|
+
/** Re-export for callers that imported redirect from this module. */
|
|
7
|
+
export declare const DEFAULT_REDIRECT_URI: "https://login.microsoftonline.com/common/oauth2/nativeclient";
|
|
8
|
+
export type SharePointUploadOverrides = {
|
|
9
|
+
MICROSOFT_TENANT_ID?: string;
|
|
10
|
+
MICROSOFT_CLIENT_ID?: string;
|
|
11
|
+
SHAREPOINT_SITE_HOST?: string;
|
|
12
|
+
SHAREPOINT_SITE_PATH?: string;
|
|
13
|
+
SHAREPOINT_UPLOAD_BASE_PATH?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Uploads when `SHAREPOINT_SITE_HOST` resolves (params and/or env).
|
|
17
|
+
* Skips when the site host is missing.
|
|
18
|
+
*/
|
|
19
|
+
export type SharePointUploadSaved = {
|
|
20
|
+
skipped: false;
|
|
21
|
+
webUrl?: string;
|
|
22
|
+
/** Microsoft Graph driveItem id (opaque). */
|
|
23
|
+
driveItemId?: string;
|
|
24
|
+
/** Present for items in SharePoint document libraries. */
|
|
25
|
+
sharepointIds?: GraphDriveItemSummary["sharepointIds"];
|
|
26
|
+
};
|
|
27
|
+
export declare function uploadBufferToSharePointIfConfigured(buffer: Buffer, filename: string, overrides?: SharePointUploadOverrides): Promise<{
|
|
28
|
+
skipped: true;
|
|
29
|
+
} | SharePointUploadSaved>;
|
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SharePoint upload via Microsoft Graph + interactive browser sign-in.
|
|
3
|
+
* See `constants.ts` for Graph scopes, redirect URI, and defaults.
|
|
4
|
+
*/
|
|
5
|
+
import { InteractiveBrowserCredential, deserializeAuthenticationRecord, serializeAuthenticationRecord, useIdentityPlugin, } from "@azure/identity";
|
|
6
|
+
import { cachePersistencePlugin } from "@azure/identity-cache-persistence";
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
import fs from "node:fs/promises";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { AADSTS65004_SNIPPET, CONSENT_DECLINED_SNIPPET, ENTRA_DEFAULT_REDIRECT_URI, ENTRA_TENANT_FALLBACK_ORGANIZATIONS, ERR_CONSENT_DECLINED_HINT, ERR_GRAPH_CREATE_FOLDER, ERR_GRAPH_FOLDER_CHECK, ERR_INTERACTIVE_NO_GRAPH_TOKEN, ERR_MICROSOFT_CLIENT_ID_REQUIRED, ERR_SHAREPOINT_NO_DRIVES, DOCUMENT_HASH_ALGORITHM, GRAPH_DELEGATED_SCOPES, HTTP_CONTENT_TYPE_JSON, MCP_SERVER_NAME, MICROSOFT_AUTH_RECORD_BASE_DIR, DEBUG_SHAREPOINT_MY_URL_ENV, MICROSOFT_GRAPH_CONFLICT_BEHAVIOR_FAIL, MICROSOFT_TOKEN_CACHE_DEFAULT_NAME, SHAREPOINT_DEFAULT_DOCUMENT_LIBRARY_NAME, MICROSOFT_REDIRECT_URI, } from "./constants.js";
|
|
12
|
+
import { encodeDriveRelativePath, graphBetaGetJson, graphGetJson, graphPutStream, graphRequest, } from "./graph-client.js";
|
|
13
|
+
/** Re-export for callers that imported redirect from this module. */
|
|
14
|
+
export const DEFAULT_REDIRECT_URI = ENTRA_DEFAULT_REDIRECT_URI;
|
|
15
|
+
let msalPersistencePluginAttempted = false;
|
|
16
|
+
let msalPersistencePluginRegistered = false;
|
|
17
|
+
function ensureMsalPersistencePlugin() {
|
|
18
|
+
if (msalPersistencePluginRegistered) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (msalPersistencePluginAttempted) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
msalPersistencePluginAttempted = true;
|
|
25
|
+
const disabled = process.env.MICROSOFT_DISABLE_TOKEN_CACHE;
|
|
26
|
+
if (disabled === "1" || disabled === "true") {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
useIdentityPlugin(cachePersistencePlugin);
|
|
31
|
+
msalPersistencePluginRegistered = true;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
36
|
+
console.error(`[${MCP_SERVER_NAME}] MSAL token cache plugin failed (${msg}). Browser sign-in may be required every run.`);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function resolveTokenCachePersistenceOptions() {
|
|
41
|
+
if (!ensureMsalPersistencePlugin()) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const name = process.env.MICROSOFT_TOKEN_CACHE_NAME?.trim() ||
|
|
45
|
+
MICROSOFT_TOKEN_CACHE_DEFAULT_NAME;
|
|
46
|
+
const unsafe = process.env.MICROSOFT_TOKEN_CACHE_UNSAFE_UNENCRYPTED === "1" ||
|
|
47
|
+
process.env.MICROSOFT_TOKEN_CACHE_UNSAFE_UNENCRYPTED === "true";
|
|
48
|
+
return {
|
|
49
|
+
enabled: true,
|
|
50
|
+
name,
|
|
51
|
+
...(unsafe ? { unsafeAllowUnencryptedStorage: true } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function pick(override, ...envKeys) {
|
|
55
|
+
if (override !== undefined) {
|
|
56
|
+
return override;
|
|
57
|
+
}
|
|
58
|
+
for (const key of envKeys) {
|
|
59
|
+
const v = process.env[key];
|
|
60
|
+
if (v !== undefined && v !== "") {
|
|
61
|
+
return v;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
function normalizeSitePath(path) {
|
|
67
|
+
const p = path.trim();
|
|
68
|
+
return p.startsWith(":") ? p.slice(1) : p;
|
|
69
|
+
}
|
|
70
|
+
function resolveSiteConfig(overrides) {
|
|
71
|
+
const siteHost = pick(overrides?.SHAREPOINT_SITE_HOST, "SHAREPOINT_SITE_HOST");
|
|
72
|
+
if (!siteHost) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const rawPath = pick(overrides?.SHAREPOINT_SITE_PATH, "SHAREPOINT_SITE_PATH") ?? "";
|
|
76
|
+
return {
|
|
77
|
+
tenantId: pick(overrides?.MICROSOFT_TENANT_ID, "MICROSOFT_TENANT_ID"),
|
|
78
|
+
clientId: pick(overrides?.MICROSOFT_CLIENT_ID, "MICROSOFT_CLIENT_ID"),
|
|
79
|
+
redirectUri: pick(undefined, "MICROSOFT_REDIRECT_URI") ?? MICROSOFT_REDIRECT_URI,
|
|
80
|
+
siteHost,
|
|
81
|
+
sitePath: normalizeSitePath(rawPath),
|
|
82
|
+
folderPath: pick(overrides?.SHAREPOINT_UPLOAD_BASE_PATH, "SHAREPOINT_UPLOAD_BASE_PATH", "SHAREPOINT_FOLDER_PATH") ?? "",
|
|
83
|
+
driveName: pick(undefined, "SHAREPOINT_DRIVE_NAME") ??
|
|
84
|
+
SHAREPOINT_DEFAULT_DOCUMENT_LIBRARY_NAME,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const GRAPH_SITE_WEB_GUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
88
|
+
/**
|
|
89
|
+
* `GET /sites/{id}` returns composite ids `hostname,{siteCollectionId},{webId}`.
|
|
90
|
+
* List subpaths like `/lists/{listId}/views` return 400 with the composite form; use the web GUID.
|
|
91
|
+
*/
|
|
92
|
+
function siteIdForListScopedRequests(siteId) {
|
|
93
|
+
const segments = siteId.split(",").map((s) => s.trim());
|
|
94
|
+
if (segments.length === 3) {
|
|
95
|
+
const webId = segments[2];
|
|
96
|
+
if (webId && GRAPH_SITE_WEB_GUID_RE.test(webId)) {
|
|
97
|
+
return webId;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const trimmed = siteId.trim();
|
|
101
|
+
if (GRAPH_SITE_WEB_GUID_RE.test(trimmed)) {
|
|
102
|
+
return trimmed;
|
|
103
|
+
}
|
|
104
|
+
return siteId;
|
|
105
|
+
}
|
|
106
|
+
function credentialBindingKey(tenantId, clientId, redirectUri) {
|
|
107
|
+
return `${tenantId}|${clientId}|${redirectUri}`;
|
|
108
|
+
}
|
|
109
|
+
function authenticationRecordFilePath(bindingKey) {
|
|
110
|
+
const dir = process.env.MICROSOFT_AUTH_RECORD_DIR?.trim() ||
|
|
111
|
+
path.join(os.homedir(), MICROSOFT_AUTH_RECORD_BASE_DIR);
|
|
112
|
+
const hash = crypto
|
|
113
|
+
.createHash(DOCUMENT_HASH_ALGORITHM)
|
|
114
|
+
.update(bindingKey)
|
|
115
|
+
.digest("hex")
|
|
116
|
+
.slice(0, 24);
|
|
117
|
+
return path.join(dir, `auth-record-${hash}.json`);
|
|
118
|
+
}
|
|
119
|
+
async function loadSavedAuthenticationRecord(bindingKey) {
|
|
120
|
+
try {
|
|
121
|
+
const raw = await fs.readFile(authenticationRecordFilePath(bindingKey), "utf-8");
|
|
122
|
+
return deserializeAuthenticationRecord(raw);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function saveAuthenticationRecord(bindingKey, record) {
|
|
129
|
+
const filePath = authenticationRecordFilePath(bindingKey);
|
|
130
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
131
|
+
await fs.writeFile(filePath, serializeAuthenticationRecord(record), "utf-8");
|
|
132
|
+
}
|
|
133
|
+
function rethrowWithConsentMessageIfNeeded(e) {
|
|
134
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
135
|
+
if (msg.includes(AADSTS65004_SNIPPET) ||
|
|
136
|
+
msg.includes(CONSENT_DECLINED_SNIPPET)) {
|
|
137
|
+
throw new Error(`${msg}${ERR_CONSENT_DECLINED_HINT}`);
|
|
138
|
+
}
|
|
139
|
+
throw e;
|
|
140
|
+
}
|
|
141
|
+
async function acquireGraphToken(tenantId, clientId, redirectUri) {
|
|
142
|
+
const tenant = tenantId?.trim() || ENTRA_TENANT_FALLBACK_ORGANIZATIONS;
|
|
143
|
+
const bindingKey = credentialBindingKey(tenant, clientId, redirectUri);
|
|
144
|
+
let record = await loadSavedAuthenticationRecord(bindingKey);
|
|
145
|
+
const persistence = resolveTokenCachePersistenceOptions();
|
|
146
|
+
const credential = new InteractiveBrowserCredential({
|
|
147
|
+
tenantId: tenant,
|
|
148
|
+
clientId,
|
|
149
|
+
redirectUri,
|
|
150
|
+
...(record ? { authenticationRecord: record } : {}),
|
|
151
|
+
...(persistence ? { tokenCachePersistenceOptions: persistence } : {}),
|
|
152
|
+
});
|
|
153
|
+
if (!record) {
|
|
154
|
+
try {
|
|
155
|
+
const interactiveRecord = await credential.authenticate([
|
|
156
|
+
...GRAPH_DELEGATED_SCOPES,
|
|
157
|
+
]);
|
|
158
|
+
if (!interactiveRecord) {
|
|
159
|
+
throw new Error(ERR_INTERACTIVE_NO_GRAPH_TOKEN);
|
|
160
|
+
}
|
|
161
|
+
await saveAuthenticationRecord(bindingKey, interactiveRecord);
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
rethrowWithConsentMessageIfNeeded(e);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const result = await credential.getToken([...GRAPH_DELEGATED_SCOPES]);
|
|
169
|
+
if (!result?.token) {
|
|
170
|
+
throw new Error(ERR_INTERACTIVE_NO_GRAPH_TOKEN);
|
|
171
|
+
}
|
|
172
|
+
return result.token;
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
rethrowWithConsentMessageIfNeeded(e);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function sitePathToGraphKey(host, sitePath) {
|
|
179
|
+
const seg = sitePath.trim();
|
|
180
|
+
if (seg === "" || seg === "/") {
|
|
181
|
+
return encodeURIComponent(`${host}:/`);
|
|
182
|
+
}
|
|
183
|
+
const normalized = seg.startsWith("/") ? seg : `/${seg}`;
|
|
184
|
+
return encodeURIComponent(`${host}:${normalized}`);
|
|
185
|
+
}
|
|
186
|
+
async function resolveSiteAndDriveIds(token, host, sitePath, driveName) {
|
|
187
|
+
const siteId = (await graphGetJson(token, `/sites/${sitePathToGraphKey(host, sitePath)}`)).id;
|
|
188
|
+
const drives = await graphGetJson(token, `/sites/${encodeURIComponent(siteId)}/drives`);
|
|
189
|
+
const drive = drives.value.find((d) => d.name === driveName) ?? drives.value[0];
|
|
190
|
+
if (!drive) {
|
|
191
|
+
throw new Error(ERR_SHAREPOINT_NO_DRIVES);
|
|
192
|
+
}
|
|
193
|
+
return { siteId, driveId: drive.id };
|
|
194
|
+
}
|
|
195
|
+
function mergeDriveItemMeta(a, b) {
|
|
196
|
+
const sharepointIds = a.sharepointIds || b.sharepointIds
|
|
197
|
+
? { ...a.sharepointIds, ...b.sharepointIds }
|
|
198
|
+
: undefined;
|
|
199
|
+
return {
|
|
200
|
+
id: b.id ?? a.id,
|
|
201
|
+
webUrl: b.webUrl ?? a.webUrl,
|
|
202
|
+
sharepointIds,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async function getDriveItemBySiteDrivePath(token, siteId, driveId, encodedRelativePath) {
|
|
206
|
+
const select = "id,webUrl,sharepointIds";
|
|
207
|
+
const metaPath = `/sites/${encodeURIComponent(siteId)}/drives/${encodeURIComponent(driveId)}/root:/${encodedRelativePath}?$select=${select}`;
|
|
208
|
+
return graphGetJson(token, metaPath);
|
|
209
|
+
}
|
|
210
|
+
function sharePointHostnameWithoutScheme(siteHost) {
|
|
211
|
+
return siteHost.replace(/^https?:\/\//i, "").replace(/\/.*$/, "");
|
|
212
|
+
}
|
|
213
|
+
/** e.g. `integrantincorp` from `integrantincorp.sharepoint.com`. */
|
|
214
|
+
function tenantKeyFromSharePointHost(hostname) {
|
|
215
|
+
const lower = hostname.toLowerCase();
|
|
216
|
+
const suffix = ".sharepoint.com";
|
|
217
|
+
if (!lower.endsWith(suffix)) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
const sub = lower.slice(0, -suffix.length);
|
|
221
|
+
const first = sub.split(".")[0];
|
|
222
|
+
return first || undefined;
|
|
223
|
+
}
|
|
224
|
+
function serverRelativeItemPathFromGraphWebUrl(itemWebUrl) {
|
|
225
|
+
const u = new URL(itemWebUrl);
|
|
226
|
+
const path = u.pathname.replace(/\/+$/, "") || u.pathname;
|
|
227
|
+
return decodeURIComponent(path);
|
|
228
|
+
}
|
|
229
|
+
function parentServerRelativePathFromItem(itemPath) {
|
|
230
|
+
const i = itemPath.lastIndexOf("/");
|
|
231
|
+
if (i <= 0) {
|
|
232
|
+
return itemPath;
|
|
233
|
+
}
|
|
234
|
+
return itemPath.slice(0, i);
|
|
235
|
+
}
|
|
236
|
+
/** Decode URL until stable so `.../Shared%2520Documents` becomes a single-encoded path for `listurl`. */
|
|
237
|
+
function normalizeUrlForSharedListParam(url) {
|
|
238
|
+
let s = url.trim();
|
|
239
|
+
let prev = "";
|
|
240
|
+
while (s !== prev && /%[0-9a-f]{2}/i.test(s)) {
|
|
241
|
+
prev = s;
|
|
242
|
+
try {
|
|
243
|
+
s = decodeURIComponent(s);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return s;
|
|
250
|
+
}
|
|
251
|
+
function buildMySharedQueryString(params) {
|
|
252
|
+
const listurl = normalizeUrlForSharedListParam(params.listurl);
|
|
253
|
+
const parts = [`listurl=${encodeURIComponent(listurl)}`];
|
|
254
|
+
if (params.viewid?.trim()) {
|
|
255
|
+
parts.push(`viewid=${encodeURIComponent(params.viewid.trim())}`);
|
|
256
|
+
}
|
|
257
|
+
parts.push(`id=${encodeURIComponent(params.id)}`);
|
|
258
|
+
parts.push(`parent=${encodeURIComponent(params.parent)}`);
|
|
259
|
+
if (params.ovuser?.trim()) {
|
|
260
|
+
parts.push(`ovuser=${encodeURIComponent(params.ovuser.trim())}`);
|
|
261
|
+
}
|
|
262
|
+
return parts.join("&");
|
|
263
|
+
}
|
|
264
|
+
async function fetchListWebRootUrl(token, siteId, listId) {
|
|
265
|
+
const sid = siteIdForListScopedRequests(siteId);
|
|
266
|
+
const list = await graphGetJson(token, `/sites/${encodeURIComponent(sid)}/lists/${encodeURIComponent(listId)}?$select=webUrl`);
|
|
267
|
+
const u = list.webUrl?.trim();
|
|
268
|
+
return u || undefined;
|
|
269
|
+
}
|
|
270
|
+
function logMySharedUrlDebug(message, err) {
|
|
271
|
+
if (process.env[DEBUG_SHAREPOINT_MY_URL_ENV] !== "1") {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const detail = err instanceof Error ? err.message : err;
|
|
275
|
+
console.error(`[${MCP_SERVER_NAME}] my/shared URL: ${message}`, detail ?? "");
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Graph list `id` + library webUrl for the document library drive.
|
|
279
|
+
* Prefer site-scoped `/sites/.../drives/.../list` (works for SharePoint libraries).
|
|
280
|
+
*/
|
|
281
|
+
async function fetchDriveDocumentLibraryList(token, siteId, driveId) {
|
|
282
|
+
const select = "$select=id,webUrl";
|
|
283
|
+
const attempts = [
|
|
284
|
+
{
|
|
285
|
+
label: "site+drive /list",
|
|
286
|
+
path: `/sites/${encodeURIComponent(siteId)}/drives/${encodeURIComponent(driveId)}/list?${select}`,
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
label: "/drives /list",
|
|
290
|
+
path: `/drives/${encodeURIComponent(driveId)}/list?${select}`,
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
label: "/drives $expand=list",
|
|
294
|
+
path: `/drives/${encodeURIComponent(driveId)}?$expand=list`,
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
for (const { label, path } of attempts) {
|
|
298
|
+
try {
|
|
299
|
+
if (path.includes("$expand=list")) {
|
|
300
|
+
const drive = await graphGetJson(token, path);
|
|
301
|
+
if (drive.list?.id) {
|
|
302
|
+
return { id: drive.list.id, webUrl: drive.list.webUrl };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
const list = await graphGetJson(token, path);
|
|
307
|
+
if (list?.id) {
|
|
308
|
+
return { id: list.id, webUrl: list.webUrl };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch (e) {
|
|
313
|
+
logMySharedUrlDebug(`${label} failed`, e);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
logMySharedUrlDebug("no list id from site/drives list endpoints or expand");
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
function pickPreferredLibraryViewId(views) {
|
|
320
|
+
if (!views.length) {
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
const preferred = views.find((v) => v.name === "All Documents") ??
|
|
324
|
+
views.find((v) => /all\s*documents/i.test(v.name ?? "")) ??
|
|
325
|
+
views.find((v) => /all\s*documents/i.test(v.displayName ?? "")) ??
|
|
326
|
+
views[0];
|
|
327
|
+
return preferred?.id;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Resolves `viewid` for `*-my.sharepoint.com/shared`. Document libraries often reject
|
|
331
|
+
* `.../lists/{id}/views` on v1; we try `$expand=views`, then v1 `/views`, then beta `/views`.
|
|
332
|
+
*/
|
|
333
|
+
async function fetchLibraryViewIdForSharedLink(token, siteId, listId) {
|
|
334
|
+
const sid = siteIdForListScopedRequests(siteId);
|
|
335
|
+
const listBase = `/sites/${encodeURIComponent(sid)}/lists/${encodeURIComponent(listId)}`;
|
|
336
|
+
try {
|
|
337
|
+
const expanded = await graphGetJson(token, `${listBase}?$expand=views($select=id,name,displayName)&$select=id`);
|
|
338
|
+
const vid = pickPreferredLibraryViewId(expanded.views ?? []);
|
|
339
|
+
if (vid) {
|
|
340
|
+
return vid;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch (e) {
|
|
344
|
+
logMySharedUrlDebug("list $expand=views (v1) failed", e);
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
const data = await graphGetJson(token, `${listBase}/views`);
|
|
348
|
+
const vid = pickPreferredLibraryViewId(data.value ?? []);
|
|
349
|
+
if (vid) {
|
|
350
|
+
return vid;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
logMySharedUrlDebug("list /views (v1) failed", e);
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
const data = await graphBetaGetJson(token, `${listBase}/views`);
|
|
358
|
+
const vid = pickPreferredLibraryViewId(data.value ?? []);
|
|
359
|
+
if (vid) {
|
|
360
|
+
return vid;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch (e) {
|
|
364
|
+
logMySharedUrlDebug("list /views (beta) failed", e);
|
|
365
|
+
}
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Browser often opens library files via
|
|
370
|
+
* `https://{tenant}-my.sharepoint.com/shared?listurl=&viewid=&id=&parent=` (and optional `ovuser`).
|
|
371
|
+
* Graph `webUrl` alone points at `*.sharepoint.com/...`, which may not match what users copy from the address bar.
|
|
372
|
+
* Omits `xsdata` / `sdata` (session tokens); those are not available server-side.
|
|
373
|
+
*/
|
|
374
|
+
async function tryBuildSharePointMySharedDocumentUrl(options) {
|
|
375
|
+
const { token, siteId, driveId, listIdFromSharepointIds, itemWebUrl, sharePointSiteHost, tenantId, } = options;
|
|
376
|
+
const host = sharePointHostnameWithoutScheme(sharePointSiteHost);
|
|
377
|
+
const tenantKey = tenantKeyFromSharePointHost(host);
|
|
378
|
+
if (!tenantKey) {
|
|
379
|
+
logMySharedUrlDebug("could not derive tenant prefix", host);
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
const libraryList = (await fetchDriveDocumentLibraryList(token, siteId, driveId)) ??
|
|
383
|
+
(listIdFromSharepointIds
|
|
384
|
+
? { id: listIdFromSharepointIds, webUrl: undefined }
|
|
385
|
+
: undefined);
|
|
386
|
+
if (!libraryList?.id) {
|
|
387
|
+
logMySharedUrlDebug("no library list id (drive list + sharepointIds exhausted)");
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
const listId = libraryList.id;
|
|
391
|
+
let listWebUrl = libraryList.webUrl?.trim();
|
|
392
|
+
if (!listWebUrl) {
|
|
393
|
+
try {
|
|
394
|
+
listWebUrl = await fetchListWebRootUrl(token, siteId, listId);
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (!listWebUrl) {
|
|
401
|
+
logMySharedUrlDebug("no list webUrl", { listId });
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
const viewId = await fetchLibraryViewIdForSharedLink(token, siteId, listId);
|
|
405
|
+
if (!viewId) {
|
|
406
|
+
logMySharedUrlDebug("no view id discovered; continuing without viewid query param", { listId });
|
|
407
|
+
}
|
|
408
|
+
let itemPath;
|
|
409
|
+
try {
|
|
410
|
+
itemPath = serverRelativeItemPathFromGraphWebUrl(itemWebUrl);
|
|
411
|
+
if (!itemPath.startsWith("/")) {
|
|
412
|
+
logMySharedUrlDebug("item path not server-relative", itemPath);
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
catch (e) {
|
|
417
|
+
logMySharedUrlDebug("parse item webUrl failed", e);
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
const parentPath = parentServerRelativePathFromItem(itemPath);
|
|
421
|
+
const base = `https://${tenantKey}-my.sharepoint.com/shared`;
|
|
422
|
+
let ovuser;
|
|
423
|
+
const tid = tenantId?.trim();
|
|
424
|
+
if (tid) {
|
|
425
|
+
try {
|
|
426
|
+
const me = await graphGetJson(token, "/me?$select=userPrincipalName");
|
|
427
|
+
const upn = me.userPrincipalName?.trim();
|
|
428
|
+
if (upn) {
|
|
429
|
+
ovuser = `${tid},${upn}`;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
/* ovuser is optional */
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const q = buildMySharedQueryString({
|
|
437
|
+
listurl: listWebUrl,
|
|
438
|
+
viewid: viewId,
|
|
439
|
+
id: itemPath,
|
|
440
|
+
parent: parentPath,
|
|
441
|
+
ovuser,
|
|
442
|
+
});
|
|
443
|
+
return `${base}?${q}`;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Creates an org-scoped **view** link via Graph; `link.webUrl` generally opens in the
|
|
447
|
+
* browser even when the raw driveItem `webUrl` does not.
|
|
448
|
+
*/
|
|
449
|
+
async function tryCreateOrganizationViewLink(token, siteId, driveId, itemId) {
|
|
450
|
+
const path = `/sites/${encodeURIComponent(siteId)}/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(itemId)}/createLink`;
|
|
451
|
+
try {
|
|
452
|
+
const res = await graphRequest(token, path, {
|
|
453
|
+
method: "POST",
|
|
454
|
+
headers: { "Content-Type": HTTP_CONTENT_TYPE_JSON },
|
|
455
|
+
body: JSON.stringify({
|
|
456
|
+
type: "view",
|
|
457
|
+
scope: "organization",
|
|
458
|
+
}),
|
|
459
|
+
});
|
|
460
|
+
if (!res.ok) {
|
|
461
|
+
logMySharedUrlDebug("createLink (organization view) failed", `${res.status} ${await res.text()}`);
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
const data = (await res.json());
|
|
465
|
+
const url = data.link?.webUrl?.trim();
|
|
466
|
+
return url || undefined;
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
logMySharedUrlDebug("createLink error", e);
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async function ensureFolderPath(token, driveId, folderPath) {
|
|
474
|
+
const segments = folderPath.split("/").filter(Boolean);
|
|
475
|
+
if (segments.length === 0) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
let pathSoFar = "";
|
|
479
|
+
for (const segment of segments) {
|
|
480
|
+
pathSoFar = pathSoFar ? `${pathSoFar}/${segment}` : segment;
|
|
481
|
+
const encoded = encodeDriveRelativePath(pathSoFar);
|
|
482
|
+
const itemPath = `/drives/${encodeURIComponent(driveId)}/root:/${encoded}`;
|
|
483
|
+
const check = await graphRequest(token, itemPath);
|
|
484
|
+
if (check.ok) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (check.status !== 404) {
|
|
488
|
+
throw new Error(ERR_GRAPH_FOLDER_CHECK(pathSoFar, check.status, await check.text()));
|
|
489
|
+
}
|
|
490
|
+
const parentPath = pathSoFar.includes("/")
|
|
491
|
+
? pathSoFar.slice(0, pathSoFar.lastIndexOf("/"))
|
|
492
|
+
: "";
|
|
493
|
+
const childrenSegment = parentPath
|
|
494
|
+
? `root:/${encodeDriveRelativePath(parentPath)}:/children`
|
|
495
|
+
: "root/children";
|
|
496
|
+
const createPath = `/drives/${encodeURIComponent(driveId)}/${childrenSegment}`;
|
|
497
|
+
const created = await graphRequest(token, createPath, {
|
|
498
|
+
method: "POST",
|
|
499
|
+
headers: { "Content-Type": HTTP_CONTENT_TYPE_JSON },
|
|
500
|
+
body: JSON.stringify({
|
|
501
|
+
name: segment,
|
|
502
|
+
folder: {},
|
|
503
|
+
"@microsoft.graph.conflictBehavior": MICROSOFT_GRAPH_CONFLICT_BEHAVIOR_FAIL,
|
|
504
|
+
}),
|
|
505
|
+
});
|
|
506
|
+
if (created.status === 409) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (!created.ok) {
|
|
510
|
+
throw new Error(ERR_GRAPH_CREATE_FOLDER(segment, parentPath, created.status, await created.text()));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
export async function uploadBufferToSharePointIfConfigured(buffer, filename, overrides) {
|
|
515
|
+
const cfg = resolveSiteConfig(overrides);
|
|
516
|
+
if (!cfg) {
|
|
517
|
+
return { skipped: true };
|
|
518
|
+
}
|
|
519
|
+
if (!cfg.clientId) {
|
|
520
|
+
throw new Error(ERR_MICROSOFT_CLIENT_ID_REQUIRED(ENTRA_DEFAULT_REDIRECT_URI));
|
|
521
|
+
}
|
|
522
|
+
const host = cfg.siteHost.replace(/^https?:\/\//, "");
|
|
523
|
+
const folder = cfg.folderPath
|
|
524
|
+
.replaceAll("\\", "/")
|
|
525
|
+
.replace(/^\/+|\/+$/g, "");
|
|
526
|
+
const token = await acquireGraphToken(cfg.tenantId, cfg.clientId, cfg.redirectUri);
|
|
527
|
+
const { siteId, driveId } = await resolveSiteAndDriveIds(token, host, cfg.sitePath, cfg.driveName);
|
|
528
|
+
await ensureFolderPath(token, driveId, folder);
|
|
529
|
+
const relativeFile = folder ? `${folder}/${filename}` : filename;
|
|
530
|
+
const encodedFile = encodeDriveRelativePath(relativeFile);
|
|
531
|
+
const uploadPath = `/sites/${encodeURIComponent(siteId)}/drives/${encodeURIComponent(driveId)}/root:/${encodedFile}:/content`;
|
|
532
|
+
const putItem = await graphPutStream(token, uploadPath, buffer);
|
|
533
|
+
const metaItem = await getDriveItemBySiteDrivePath(token, siteId, driveId, encodedFile);
|
|
534
|
+
const merged = mergeDriveItemMeta(putItem, metaItem);
|
|
535
|
+
let webUrl = merged.webUrl;
|
|
536
|
+
const itemId = merged.id;
|
|
537
|
+
if (itemId) {
|
|
538
|
+
const openUrl = await tryCreateOrganizationViewLink(token, siteId, driveId, itemId);
|
|
539
|
+
if (openUrl) {
|
|
540
|
+
webUrl = openUrl;
|
|
541
|
+
}
|
|
542
|
+
else if (merged.webUrl) {
|
|
543
|
+
const modern = await tryBuildSharePointMySharedDocumentUrl({
|
|
544
|
+
token,
|
|
545
|
+
siteId,
|
|
546
|
+
driveId,
|
|
547
|
+
listIdFromSharepointIds: merged.sharepointIds?.listId,
|
|
548
|
+
itemWebUrl: merged.webUrl,
|
|
549
|
+
sharePointSiteHost: cfg.siteHost,
|
|
550
|
+
tenantId: cfg.tenantId,
|
|
551
|
+
});
|
|
552
|
+
if (modern) {
|
|
553
|
+
webUrl = modern;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
else if (merged.webUrl) {
|
|
558
|
+
const modern = await tryBuildSharePointMySharedDocumentUrl({
|
|
559
|
+
token,
|
|
560
|
+
siteId,
|
|
561
|
+
driveId,
|
|
562
|
+
listIdFromSharepointIds: merged.sharepointIds?.listId,
|
|
563
|
+
itemWebUrl: merged.webUrl,
|
|
564
|
+
sharePointSiteHost: cfg.siteHost,
|
|
565
|
+
tenantId: cfg.tenantId,
|
|
566
|
+
});
|
|
567
|
+
if (modern) {
|
|
568
|
+
webUrl = modern;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
skipped: false,
|
|
573
|
+
webUrl,
|
|
574
|
+
driveItemId: merged.id,
|
|
575
|
+
sharepointIds: merged.sharepointIds,
|
|
576
|
+
};
|
|
577
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@habibakhaledm/sharepoint-document-upload-mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "A MCP server for uploading document to Sharepoint",
|
|
5
|
-
"main": "src/server.
|
|
5
|
+
"main": "dist/src/server.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"sharepoint-document-upload-mcp": "dist/server.js"
|
|
7
|
+
"sharepoint-document-upload-mcp": "dist/src/server.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"
|
|
10
|
+
"dist/**/*.js",
|
|
11
|
+
"dist/**/*.d.ts",
|
|
11
12
|
"src/**/*.ts",
|
|
12
13
|
"tsconfig.json"
|
|
13
14
|
],
|
package/src/server.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
4
|
import { z } from "zod";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
5
|
|
|
7
6
|
import {
|
|
8
7
|
MCP_SERVER_DESCRIPTION,
|