@alliottech/sharepoint-mcp-server
Version:
A Model Context Protocol server for browsing and interacting with Microsoft SharePoint sites and documents
516 lines (515 loc) • 19.8 kB
JavaScript
/**
* SharePoint MCP Server
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, McpError, ErrorCode, } from "@modelcontextprotocol/sdk/types.js";
/**
* Environment variables required for SharePoint authentication
*/
const { SHAREPOINT_URL, TENANT_ID, CLIENT_ID, CLIENT_SECRET } = process.env;
if (!SHAREPOINT_URL || !TENANT_ID || !CLIENT_ID || !CLIENT_SECRET) {
throw new Error("Required environment variables: SHAREPOINT_URL, TENANT_ID, CLIENT_ID, CLIENT_SECRET");
}
/**
* SharePoint MCP Server implementation
* Provides tools and resources for interacting with Microsoft SharePoint via Microsoft Graph API
*/
class SharePointServer {
server;
accessToken = null;
tokenExpiry = 0;
constructor() {
this.server = new Server({
name: "sharepoint-mcp-server",
version: "0.1.0",
}, {
capabilities: {
tools: {},
resources: {},
},
});
this.setupHandlers();
this.setupErrorHandling();
}
/**
* Get access token for Microsoft Graph API
*/
async getAccessToken() {
if (this.accessToken && Date.now() < this.tokenExpiry) {
return this.accessToken;
}
const tenantId = TENANT_ID;
const clientId = CLIENT_ID;
const clientSecret = CLIENT_SECRET;
const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
const params = new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
scope: "https://graph.microsoft.com/.default",
grant_type: "client_credentials",
});
try {
const response = await fetch(tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: params,
});
if (!response.ok) {
throw new Error(`Token request failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
this.accessToken = data.access_token;
this.tokenExpiry = Date.now() + (data.expires_in * 1000) - 60000; // Refresh 1 minute early
return this.accessToken;
}
catch (error) {
throw new Error(`Failed to get access token: ${error}`);
}
}
/**
* Make authenticated request to Microsoft Graph API
*/
async graphRequest(endpoint, method = "GET", body) {
const token = await this.getAccessToken();
const url = `https://graph.microsoft.com/v1.0${endpoint}`;
const headers = {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
};
const options = {
method,
headers,
};
if (body && method !== "GET") {
options.body = JSON.stringify(body);
}
try {
const response = await fetch(url, options);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Graph API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
return await response.json();
}
catch (error) {
throw new Error(`Graph API request error: ${error}`);
}
}
/**
* Setup error handling for the server
*/
setupErrorHandling() {
this.server.onerror = (error) => console.error("[MCP Error]", error);
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
/**
* Setup all request handlers for tools and resources
*/
setupHandlers() {
this.setupToolHandlers();
this.setupResourceHandlers();
}
/**
* Setup tool handlers for SharePoint operations
*/
setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "search_files",
description: "Search for files and documents in SharePoint using Microsoft Graph Search API",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "The search query string",
},
limit: {
type: "number",
description: "Maximum number of results to return (default: 10)",
default: 10,
},
},
required: ["query"],
},
},
{
name: "list_sites",
description: "List SharePoint sites accessible to the application",
inputSchema: {
type: "object",
properties: {
search: {
type: "string",
description: "Optional search term to filter sites",
},
},
},
},
{
name: "get_site_info",
description: "Get detailed information about a specific SharePoint site",
inputSchema: {
type: "object",
properties: {
siteUrl: {
type: "string",
description: "The SharePoint site URL (e.g., https://tenant.sharepoint.com/sites/sitename)",
},
},
required: ["siteUrl"],
},
},
{
name: "list_site_drives",
description: "List document libraries (drives) in a SharePoint site",
inputSchema: {
type: "object",
properties: {
siteUrl: {
type: "string",
description: "The SharePoint site URL",
},
},
required: ["siteUrl"],
},
},
{
name: "list_drive_items",
description: "List files and folders in a SharePoint document library",
inputSchema: {
type: "object",
properties: {
siteUrl: {
type: "string",
description: "The SharePoint site URL",
},
driveId: {
type: "string",
description: "The drive ID (optional, uses default drive if not specified)",
},
folderPath: {
type: "string",
description: "Optional folder path to list items from (default: root)",
},
},
required: ["siteUrl"],
},
},
{
name: "get_file_content",
description: "Get the content of a specific file from SharePoint (text files only)",
inputSchema: {
type: "object",
properties: {
siteUrl: {
type: "string",
description: "The SharePoint site URL",
},
filePath: {
type: "string",
description: "The path to the file",
},
driveId: {
type: "string",
description: "The drive ID (optional, uses default drive if not specified)",
},
},
required: ["siteUrl", "filePath"],
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
switch (request.params.name) {
case "search_files":
return await this.handleSearchFiles(request.params.arguments);
case "list_sites":
return await this.handleListSites(request.params.arguments);
case "get_site_info":
return await this.handleGetSiteInfo(request.params.arguments);
case "list_site_drives":
return await this.handleListSiteDrives(request.params.arguments);
case "list_drive_items":
return await this.handleListDriveItems(request.params.arguments);
case "get_file_content":
return await this.handleGetFileContent(request.params.arguments);
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new McpError(ErrorCode.InternalError, `SharePoint operation failed: ${errorMessage}`);
}
});
}
/**
* Setup resource handlers for SharePoint resources
*/
setupResourceHandlers() {
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
try {
const response = await this.graphRequest("/sites?$select=id,displayName,name,webUrl");
const sites = response.value || [];
return {
resources: sites.map((site) => ({
uri: `sharepoint://site/${site.id}`,
mimeType: "application/json",
name: site.displayName || site.name,
description: `SharePoint site: ${site.displayName || site.name} (${site.webUrl})`,
})),
};
}
catch (error) {
console.error("Error listing resources:", error);
return { resources: [] };
}
});
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const url = new URL(request.params.uri);
if (url.protocol === "sharepoint:" && url.pathname.startsWith("/site/")) {
const siteId = url.pathname.replace("/site/", "");
try {
const site = await this.graphRequest(`/sites/${siteId}`);
return {
contents: [{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(site, null, 2),
}],
};
}
catch (error) {
throw new McpError(ErrorCode.InternalError, `Failed to read site resource: ${error}`);
}
}
throw new McpError(ErrorCode.InvalidParams, `Unsupported resource URI: ${request.params.uri}`);
});
}
/**
* Extract site ID from SharePoint URL
*/
async getSiteIdFromUrl(siteUrl) {
try {
const url = new URL(siteUrl);
const hostname = url.hostname;
const pathname = url.pathname;
const response = await this.graphRequest(`/sites/${hostname}:${pathname}`);
return response.id;
}
catch (error) {
throw new Error(`Failed to get site ID from URL ${siteUrl}: ${error}`);
}
}
/**
* Handle search files tool request
*/
async handleSearchFiles(args) {
const query = args?.query;
const limit = args?.limit || 10;
if (typeof query !== "string") {
throw new McpError(ErrorCode.InvalidParams, "Query parameter must be a string");
}
try {
const searchRequest = {
requests: [{
entityTypes: ["driveItem"],
query: {
queryString: query,
},
size: limit,
}],
};
const searchResults = await this.graphRequest("/search/query", "POST", searchRequest);
return {
content: [{
type: "text",
text: JSON.stringify(searchResults, null, 2),
}],
};
}
catch (error) {
throw new Error(`Search failed: ${error}`);
}
}
/**
* Handle list sites tool request
*/
async handleListSites(args) {
const searchTerm = args?.search;
try {
let endpoint = "/sites?$select=id,displayName,name,webUrl,description";
if (searchTerm) {
endpoint += `&$filter=contains(displayName,'${searchTerm}')`;
}
const response = await this.graphRequest(endpoint);
const sites = response.value || [];
return {
content: [{
type: "text",
text: JSON.stringify(sites, null, 2),
}],
};
}
catch (error) {
throw new Error(`Failed to list sites: ${error}`);
}
}
/**
* Handle get site info tool request
*/
async handleGetSiteInfo(args) {
const siteUrl = args?.siteUrl;
if (typeof siteUrl !== "string") {
throw new McpError(ErrorCode.InvalidParams, "siteUrl parameter must be a string");
}
try {
const siteId = await this.getSiteIdFromUrl(siteUrl);
const site = await this.graphRequest(`/sites/${siteId}?$expand=drive`);
return {
content: [{
type: "text",
text: JSON.stringify(site, null, 2),
}],
};
}
catch (error) {
throw new Error(`Failed to get site info: ${error}`);
}
}
/**
* Handle list site drives tool request
*/
async handleListSiteDrives(args) {
const siteUrl = args?.siteUrl;
if (typeof siteUrl !== "string") {
throw new McpError(ErrorCode.InvalidParams, "siteUrl parameter must be a string");
}
try {
const siteId = await this.getSiteIdFromUrl(siteUrl);
const response = await this.graphRequest(`/sites/${siteId}/drives`);
const drives = response.value || [];
return {
content: [{
type: "text",
text: JSON.stringify(drives, null, 2),
}],
};
}
catch (error) {
throw new Error(`Failed to list site drives: ${error}`);
}
}
/**
* Handle list drive items tool request
*/
async handleListDriveItems(args) {
const siteUrl = args?.siteUrl;
const driveId = args?.driveId;
const folderPath = args?.folderPath;
if (typeof siteUrl !== "string") {
throw new McpError(ErrorCode.InvalidParams, "siteUrl parameter must be a string");
}
try {
const siteId = await this.getSiteIdFromUrl(siteUrl);
let endpoint;
if (driveId) {
if (folderPath) {
endpoint = `/sites/${siteId}/drives/${driveId}/root:/${folderPath}:/children`;
}
else {
endpoint = `/sites/${siteId}/drives/${driveId}/root/children`;
}
}
else {
if (folderPath) {
endpoint = `/sites/${siteId}/drive/root:/${folderPath}:/children`;
}
else {
endpoint = `/sites/${siteId}/drive/root/children`;
}
}
const response = await this.graphRequest(endpoint);
const items = response.value || [];
return {
content: [{
type: "text",
text: JSON.stringify(items, null, 2),
}],
};
}
catch (error) {
throw new Error(`Failed to list drive items: ${error}`);
}
}
/**
* Handle get file content tool request
*/
async handleGetFileContent(args) {
const siteUrl = args?.siteUrl;
const filePath = args?.filePath;
const driveId = args?.driveId;
if (typeof siteUrl !== "string" || typeof filePath !== "string") {
throw new McpError(ErrorCode.InvalidParams, "siteUrl and filePath parameters must be strings");
}
try {
const siteId = await this.getSiteIdFromUrl(siteUrl);
let endpoint;
if (driveId) {
endpoint = `/sites/${siteId}/drives/${driveId}/root:/${filePath}:/content`;
}
else {
endpoint = `/sites/${siteId}/drive/root:/${filePath}:/content`;
}
const token = await this.getAccessToken();
const response = await fetch(`https://graph.microsoft.com/v1.0${endpoint}`, {
headers: {
"Authorization": `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error(`Failed to get file content: ${response.status} ${response.statusText}`);
}
const content = await response.text();
return {
content: [{
type: "text",
text: content,
}],
};
}
catch (error) {
throw new Error(`Failed to get file content: ${error}`);
}
}
/**
* Start the MCP server
*/
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("SharePoint MCP server running on stdio");
}
}
/**
* Main entry point
*/
const server = new SharePointServer();
server.run().catch((error) => {
console.error("Failed to start SharePoint MCP server:", error);
process.exit(1);
});