# Directory Structure
```
├── .env.example
├── .gitignore
├── Dockerfile
├── eslint.config.ts
├── package.json
├── pnpm-lock.yaml
├── README.md
├── src
│ └── index.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
1 | TENANT_ID=your-tenant-id
2 | CLIENT_ID=your-client-id
3 | CLIENT_SECRET=your-client-secret
4 | SITE_ID=your-site-id
5 | DRIVE_ID=your-drive-id
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # ts gitignore
2 |
3 | # Ignore build output
4 | dist/
5 | build/
6 | node_modules/
7 |
8 | # Ignore local config
9 | config.local.ts
10 |
11 | # Ignore IDE files
12 | .vscode/
13 | .idea/
14 |
15 | # Ignore test output
16 | test-output/
17 |
18 | # Ignore logs
19 | logs/
20 |
21 | # Ignore coverage
22 | coverage/
23 |
24 | # Ignore package-lock.json
25 | package-lock.json
26 |
27 | # Ignore .env
28 | .env
29 |
30 | # folders
31 | /output
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Sharepoint - WIP, just for R&D ATM
2 |
3 | A Model Context Protocol server that provides access to Organisational Sharepoint.
4 |
5 | ## Implementation
6 |
7 | | Component | Operation | Resource | Dynamic Resource | Tool |
8 | |--------------------|---------------------|----------|------------------|------|
9 | | Users | | ❌ | ❌ | ❌ |
10 | | | Read User | ❌ | ❌ | ❌ |
11 | | | Find User | ❌ | ❌ | ❌ |
12 | | Sites | | ❌ | ❌ | ❌ |
13 | | | List Sites | ✅ | ❌ | ❌ |
14 | | | Get Site Details | ❌ | ❌ | ❌ |
15 | | | Create Subsite | ❌ | ❌ | ❌ |
16 | | | Delete Site | ❌ | ❌ | ❌ |
17 | | Drives | | ❌ | ❌ | ❌ |
18 | | | List Folders | ❌ | ❌ | ❌ |
19 | | | Search Folders | ❌ | ❌ | ✅ |
20 | | | Create Folder | ❌ | ❌ | ❌ |
21 | | | Delete Folder | ❌ | ❌ | ❌ |
22 | | | Upload File | ❌ | ❌ | ❌ |
23 | | | List Items | ❌ | ✅ | ❌ |
24 | | | Download File | ❌ | ❌ | ✅ |
25 | | | Read File | ✅ | ❌ | ❌ |
26 | | | Move File | ❌ | ❌ | ❌ |
27 | | | Copy File | ❌ | ❌ | ❌ |
28 | | Lists | | ❌ | ❌ | ❌ |
29 | | | Create List | ❌ | ❌ | ❌ |
30 | | | Read List | ❌ | ❌ | ❌ |
31 | | | Add to List | ❌ | ❌ | ❌ |
32 | | | Update List | ❌ | ❌ | ❌ |
33 | | | Delete List | ❌ | ❌ | ❌ |
34 | | Calendar | | ❌ | ❌ | ❌ |
35 | | | Create Event | ❌ | ❌ | ❌ |
36 | | | Read Event | ❌ | ❌ | ❌ |
37 | | | Update Event | ❌ | ❌ | ❌ |
38 | | | Delete Event | ❌ | ❌ | ❌ |
39 |
40 | ### Prompts
41 |
42 | - document-summary
43 | - find-relevant-documents
44 | - explore-folder
45 |
46 | ## Enviremental Variables
47 |
48 | - Copy .env.example as .env
49 | - Fill the requires fields
50 |
51 | ## Inspector
52 |
53 | From root
54 |
55 | ```Bash
56 | npx @modelcontextprotocol/inspector -e TENANT_ID=your_tenant_id -e CLIENT_ID=your_client_id -e CLIENT_SECRET=your_client_secret -e SITE_ID=your_site_id -e DRIVE_ID=your_drive_id -- node dist/index.js
57 | ```
58 |
59 | ## Usage with Claude Desktop
60 |
61 | To use this server with the Claude Desktop app, add the following configuration to the "mcpServers" section of your `claude_desktop_config.json`:
62 |
63 | ### Docker
64 |
65 | * Docker build and tag `docker build -t mcp/sharepoint .`
66 |
67 | ```json
68 | {
69 | "mcpServers": {
70 | "sharepoint": {
71 | "command": "docker",
72 | "args": [
73 | "run",
74 | "-i",
75 | "--rm",
76 | "--init",
77 | "-e", "DOCKER_CONTAINER=true",
78 | "-e", "TENANT_ID=your-tenant-id",
79 | "-e", "CLIENT_ID=your-client-id",
80 | "-e", "CLIENT_SECRET=your-client-secret",
81 | "-e", "SITE_ID=your-site-id",
82 | "-e", "DRIVE_ID=your-drive-id",
83 | "mcp/sharepoint"
84 | ]
85 | }
86 | }
87 | }
88 | ```
89 | ### MCP configuration file
90 |
91 | ```bash
92 | pnpm run build
93 | ```
94 |
95 | ```json
96 | {
97 | "mcpServers": {
98 | "sharepoint": {
99 | "command": "node",
100 | "args": ["run", "start"],
101 | "env": {
102 | "TENANT_ID": "your-tenant-id",
103 | "CLIENT_ID": "your-client-id",
104 | "CLIENT_SECRET": "your-client-secret",
105 | "SITE_ID": "your-site-id",
106 | "DRIVE_ID": "your-drive-id",
107 | }
108 | }
109 | }
110 | }
111 | ```
112 |
113 | ## License
114 |
115 | This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
116 |
```
--------------------------------------------------------------------------------
/eslint.config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import eslint from '@eslint/js';
2 | import tseslint from 'typescript-eslint';
3 |
4 | export default tseslint.config(
5 | eslint.configs.recommended,
6 | tseslint.configs.recommended,
7 | tseslint.configs.strictTypeChecked,
8 | tseslint.configs.stylistic,
9 | {
10 | files: ["src/**/*.ts"],
11 | ignores: ['node_modules', 'dist', 'build'],
12 | }
13 | );
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2024",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "outDir": "./dist",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "noImplicitAny": true,
12 | "forceConsistentCasingInFileNames": true
13 | },
14 | "include": ["src/**/*"],
15 | "exclude": ["node_modules", "dist"]
16 | }
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "sharepoint",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "type": "module",
7 | "bin": {
8 | "sharepoint": "./dist/index.js"
9 | },
10 | "scripts": {
11 | "dev": "tsx watch src/index.ts",
12 | "build": "tsc",
13 | "start": "node dist/index.js"
14 | },
15 | "keywords": [],
16 | "author": "",
17 | "license": "ISC",
18 | "dependencies": {
19 | "@azure/identity": "^4.8.0",
20 | "@microsoft/microsoft-graph-client": "^3.0.7",
21 | "@modelcontextprotocol/sdk": "^1.9.0",
22 | "dotenv": "^16.4.7",
23 | "microsoft-graph": "^2.8.0",
24 | "zod": "^3.24.2"
25 | },
26 | "devDependencies": {
27 | "@eslint/js": "^9.23.0",
28 | "@types/node": "^22.13.10",
29 | "eslint": "^9.23.0",
30 | "globals": "^16.0.0",
31 | "jiti": "^2.4.2",
32 | "typescript": "^5.8.2",
33 | "typescript-eslint": "^8.28.0",
34 | "tsx": "^4.19.3"
35 | }
36 | }
37 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Use the official Node.js image as the base image
2 | FROM node:22-slim AS builder
3 |
4 | # Install pnpm globally
5 | RUN corepack enable && corepack prepare pnpm@latest --activate
6 |
7 | # Set the working directory inside the container
8 | WORKDIR /app
9 |
10 | # Copy the source code and configuration files
11 | COPY src /app/src
12 | COPY tsconfig.json /app/tsconfig.json
13 | COPY package.json /app/package.json
14 | COPY pnpm-lock.yaml /app/pnpm-lock.yaml
15 |
16 | # Install dependencies using pnpm
17 | RUN pnpm install
18 |
19 | # Compile TypeScript to JavaScript
20 | RUN pnpm run build
21 |
22 | # Use a lightweight Node.js runtime image for the release stage
23 | FROM node:22-slim AS release
24 |
25 | # Install pnpm globally
26 | RUN corepack enable && corepack prepare pnpm@latest --activate
27 |
28 | # Set the working directory inside the container
29 | WORKDIR /app
30 |
31 | # Copy the compiled code and dependencies from the builder stage
32 | COPY --from=builder /app /app
33 |
34 | # Set the environment to production
35 | ENV NODE_ENV=production
36 |
37 | # Run the application
38 | ENTRYPOINT ["pnpm", "start"]
39 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3 | import { z } from "zod";
4 | import { register } from "microsoft-graph/services/context";
5 | import { getEnvironmentVariable } from "microsoft-graph/services/environmentVariable";
6 | import { TenantId } from "microsoft-graph/models/TenantId";
7 | import { ClientId } from "microsoft-graph/models/ClientId";
8 | import { SiteId } from "microsoft-graph/models/SiteId";
9 | import { ClientSecret } from "microsoft-graph/models/ClientSecret";
10 | import dotenv from "dotenv";
11 |
12 | import { createDriveRef } from "microsoft-graph/services/drive";
13 | import { createSiteRef } from "microsoft-graph/services/site";
14 | import { DriveId } from "microsoft-graph/models/DriveId";
15 | import listDriveItems from "microsoft-graph/operations/driveItem/listDriveItems";
16 | import { createDriveItemRef } from "microsoft-graph/services/driveItem";
17 | import { DriveItemId } from "microsoft-graph/models/DriveItemId";
18 | import getDriveItem from "microsoft-graph/operations/driveItem/getDriveItem";
19 | import listSites from "microsoft-graph/operations/site/listSites";
20 | dotenv.config();
21 |
22 | // Initialize the MCP server and SharePoint connector
23 | async function createSharepointMcpServer() {
24 |
25 | const tenantId = getEnvironmentVariable("TENANT_ID") as TenantId;
26 | const clientId = getEnvironmentVariable("CLIENT_ID") as ClientId;
27 | const clientSecret = getEnvironmentVariable("CLIENT_SECRET") as ClientSecret;
28 |
29 | const driveId = getEnvironmentVariable("DRIVE_ID") as DriveId;
30 | const siteId = getEnvironmentVariable("SITE_ID") as SiteId;
31 | // Create the server
32 | const server = new McpServer({
33 | name: "SharePoint Server",
34 | version: "1.0.0"
35 | });
36 |
37 | const contextRef = register(tenantId, clientId, clientSecret);
38 | const siteRef = createSiteRef(contextRef, siteId);
39 | const driveRef = createDriveRef(siteRef, driveId);
40 |
41 | // Resource: Folder contents (root or specific folder)
42 | server.resource(
43 | "folder",
44 | new ResourceTemplate("sharepoint://folder/{folderId?}", { list: undefined }),
45 | async (uri) => {
46 | const items = await listDriveItems(driveRef);
47 | return {
48 | contents: [{
49 | uri: uri.href,
50 | text: JSON.stringify(items, null, 2)
51 | }]
52 | };
53 | }
54 | );
55 |
56 | // Resource: Sites
57 | server.resource(
58 | "sites",
59 | "sharepoint://sites",
60 | async (uri) => {
61 | const items = await listSites(contextRef);
62 | return {
63 | contents: [{
64 | uri: uri.href,
65 | text: JSON.stringify(items, null, 2)
66 | }]
67 | };
68 | }
69 | );
70 |
71 | // Resource: Document content
72 | server.resource(
73 | "document",
74 | new ResourceTemplate("sharepoint://document/{documentId}", { list: undefined }),
75 | async (uri, { documentId }) => {
76 | const driveItemRef = createDriveItemRef(driveRef, documentId as DriveItemId);
77 | const result = await getDriveItem(driveItemRef);
78 | return {
79 | contents: [{
80 | uri: uri.href,
81 | text: result.content
82 | ? (typeof result.content === 'string'
83 | ? result.content
84 | : JSON.stringify(result.content, null, 2))
85 | : "No content available" // Fallback value
86 | }]
87 | };
88 | }
89 | );
90 |
91 | // Tool: Search for documents
92 | server.tool(
93 | "search-documents",
94 | {
95 | query: z.string().describe("Search query to find documents"),
96 | maxResults: z.string().optional().describe("Maximum number of results to return (as a string)")
97 | },
98 | async ({ query, maxResults = 10 }) => {
99 | try {
100 | const driveRef = createDriveRef(siteRef, driveId);
101 | const driveItems = await listDriveItems(driveRef);
102 | const results = driveItems.filter((item: any) => item.name.includes(query)).slice(0, parseInt(maxResults.toString(), 10));
103 | return {
104 | content: [{
105 | type: "text",
106 | text: JSON.stringify(results, null, 2) // Ensure this is a valid string
107 | }]
108 | };
109 | } catch (error) {
110 | return {
111 | content: [{
112 | type: "text",
113 | text: `Error searching documents: ${error}`
114 | }],
115 | isError: true
116 | };
117 | }
118 | }
119 | );
120 |
121 | // Download document content
122 | server.tool(
123 | "download-document",
124 | {
125 | documentId: z.string().describe("The ID of the document to download")
126 | },
127 | async ({ documentId }) => {
128 | try {
129 | const driveItemRef = createDriveItemRef(driveRef, documentId as DriveItemId);
130 | const result = await getDriveItem(driveItemRef);
131 | return {
132 | content: [{
133 | type: "text",
134 | text: result.content
135 | ? (typeof result.content === 'string'
136 | ? result.content
137 | : JSON.stringify(result.content, null, 2))
138 | : "No content available" // Fallback value
139 | }]
140 | };
141 | } catch (error) {
142 | return {
143 | content: [{
144 | type: "text",
145 | text: `Error downloading document: ${error}`
146 | }],
147 | isError: true
148 | };
149 | }
150 | }
151 | );
152 |
153 | // Prompt: Search and summarize a document
154 | server.prompt(
155 | "document-summary",
156 | {
157 | documentId: z.string().describe("The ID of the document to summarize")
158 | },
159 | ({ documentId }) => ({
160 | messages: [{
161 | role: "user",
162 | content: {
163 | type: "text",
164 | text: `Please retrieve the document with ID ${documentId} using the sharepoint://document/${documentId} resource, then provide a concise summary of its key points, main topics, and important information.`
165 | }
166 | }]
167 | })
168 | );
169 |
170 | // Prompt: Find relevant documents
171 | server.prompt(
172 | "find-relevant-documents",
173 | {
174 | topic: z.string().describe("The topic or subject to find documents about"),
175 | maxResults: z.string().optional().describe("Maximum number of results to return (as a string)")
176 | },
177 | ({ topic, maxResults = "5" }) => ({
178 | messages: [{
179 | role: "user",
180 | content: {
181 | type: "text",
182 | text: `Please use the search-documents tool to find up to ${maxResults} documents related to "${topic}". For each document, provide the title, author, last modified date, and a brief description of what it appears to contain based on the metadata.`
183 | }
184 | }]
185 | })
186 | );
187 |
188 | // Prompt: Explore folder contents
189 | server.prompt(
190 | "explore-folder",
191 | {
192 | folderId: z.string().optional().describe("The ID of the folder to explore (leave empty for root folder)")
193 | },
194 | ({ folderId }) => ({
195 | messages: [{
196 | role: "user",
197 | content: {
198 | type: "text",
199 | text: folderId
200 | ? `Please explore the contents of the folder with ID ${folderId} using the sharepoint://folder/${folderId} resource. List all documents and subfolders, organizing them by type and providing key details about each item.`
201 | : `Please explore the contents of the root folder using the sharepoint://folder resource. List all documents and subfolders, organizing them by type and providing key details about each item.`
202 | }
203 | }]
204 | })
205 | );
206 |
207 | return server;
208 | }
209 |
210 | // Example usage
211 | async function main() {
212 |
213 | // Create and start the server
214 | const server = await createSharepointMcpServer();
215 |
216 | // Connect using stdio transport
217 | const transport = new StdioServerTransport();
218 | await server.connect(transport);
219 | }
220 |
221 | // Run the server
222 | main().catch(error => {
223 | console.error("Error starting server:", error);
224 | process.exit(1);
225 | });
```