UNPKG

@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
#!/usr/bin/env node /** * 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); });