├── .gitignore ├── tsconfig.json ├── Dockerfile ├── package.json ├── .github └── workflows │ ├── npm.yml │ └── docker.yml ├── README.md └── src └── index.ts /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "outDir": "./dist", 12 | "rootDir": "./src" 13 | }, 14 | "include": [ 15 | "./src/**/*.ts" 16 | ], 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.12-alpine AS builder 2 | 3 | # Must be entire project because `prepare` script is run during `npm install` and requires all files. 4 | COPY . /app 5 | COPY tsconfig.json /tsconfig.json 6 | 7 | WORKDIR /app 8 | 9 | RUN --mount=type=cache,target=/root/.npm npm install 10 | 11 | RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev 12 | 13 | FROM node:22-alpine AS release 14 | 15 | COPY --from=builder /app/dist /app/dist 16 | COPY --from=builder /app/package.json /app/package.json 17 | COPY --from=builder /app/package-lock.json /app/package-lock.json 18 | 19 | ENV NODE_ENV=production 20 | 21 | WORKDIR /app 22 | 23 | RUN npm ci --ignore-scripts --omit-dev 24 | 25 | ENTRYPOINT ["node", "dist/index.js"] 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@infomaniak/mcp-server-kdrive", 3 | "version": "0.0.1", 4 | "description": "MCP server for interacting with Infomaniak kDrive", 5 | "license": "MIT", 6 | "author": "Infonaniak (https://infomaniak.com)", 7 | "homepage": "https://infomaniak.com", 8 | "bugs": "https://github.com/infomaniak/mcp-server-kdrive/issues", 9 | "type": "module", 10 | "bin": { 11 | "mcp-server-kdrive": "dist/index.js" 12 | }, 13 | "files": [ 14 | "dist" 15 | ], 16 | "scripts": { 17 | "build": "tsc && shx chmod +x dist/*.js", 18 | "prepare": "npm run build", 19 | "watch": "tsc --watch" 20 | }, 21 | "dependencies": { 22 | "@modelcontextprotocol/sdk": "1.12.0", 23 | "zod": "3.25.20" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^22", 27 | "shx": "^0.3.4", 28 | "typescript": "^5.6.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/npm.yml: -------------------------------------------------------------------------------- 1 | name: TypeScript 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: 22 21 | cache: npm 22 | 23 | - name: Install dependencies 24 | run: npm ci 25 | 26 | - name: Build package 27 | run: npm run build 28 | 29 | publish: 30 | runs-on: ubuntu-latest 31 | needs: [build] 32 | if: github.event_name == 'release' 33 | environment: release 34 | 35 | name: Publish 36 | 37 | permissions: 38 | contents: read 39 | id-token: write 40 | 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: actions/setup-node@v4 44 | with: 45 | node-version: 22 46 | cache: npm 47 | registry-url: "https://registry.npmjs.org" 48 | 49 | - name: Install dependencies 50 | run: npm ci 51 | 52 | - name: Publish package 53 | run: npm publish --access public 54 | env: 55 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 56 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Create and publish a Docker image 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | jobs: 13 | build-and-push-image: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Log in to the Container registry 24 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 25 | with: 26 | registry: ${{ env.REGISTRY }} 27 | username: ${{ github.actor }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Extract metadata (tags, labels) for Docker 31 | id: meta 32 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 33 | with: 34 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 35 | 36 | - name: Build and push Docker image 37 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 38 | with: 39 | context: . 40 | push: true 41 | tags: ${{ steps.meta.outputs.tags }} 42 | labels: ${{ steps.meta.outputs.labels }} 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kDrive MCP Server 2 | 3 | MCP Server for the kDrive API. 4 | 5 | ## Tools 6 | 7 | 1. `kdrive_search` 8 | - Search in kDrive 9 | - Required inputs: 10 | - `query` (string): Search query 11 | - Returns: List of files 12 | 13 | ## Setup 14 | 15 | 1. Create a kDrive token linked to your user: 16 | - Visit the [API Token page](https://manager.infomaniak.com/v3/ng/accounts/token/list) 17 | - Choose "drive" scope 18 | 19 | ### Usage with Claude Desktop 20 | 21 | Add the following to your `claude_desktop_config.json`: 22 | 23 | #### NPX 24 | 25 | ```json 26 | { 27 | "mcpServers": { 28 | "kdrive": { 29 | "command": "npx", 30 | "args": [ 31 | "-y", 32 | "@infomaniak/mcp-server-kdrive" 33 | ], 34 | "env": { 35 | "KDRIVE_TOKEN": "your-token", 36 | "KDRIVE_ID": "your-kdrive-id" 37 | } 38 | } 39 | } 40 | } 41 | ``` 42 | 43 | #### docker 44 | 45 | ```json 46 | { 47 | "mcpServers": { 48 | "kdrive": { 49 | "command": "docker", 50 | "args": [ 51 | "run", 52 | "-i", 53 | "--rm", 54 | "-e", 55 | "KDRIVE_TOKEN", 56 | "-e", 57 | "KDRIVE_ID", 58 | "infomaniak/mcp-server-kchat" 59 | ], 60 | "env": { 61 | "KDRIVE_TOKEN": "your-token", 62 | "KDRIVE_ID": "your-kdrive-id" 63 | } 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | ### Environment Variables 70 | 71 | 1. `KDRIVE_TOKEN`: Required. Your kDrive token. 72 | 2. `KDRIVE_ID`: Required. Your kDrive id fetch from the webapp URL. (eg. if your kDrive webapp url is https://ksuite.infomaniak.com/all/kdrive/app/drive/12 your drive id is 12) 73 | 74 | ### Troubleshooting 75 | 76 | If you encounter permission errors, verify that: 77 | 1. All required scopes are added to your drive token 78 | 2. The token and drive id are correctly copied to your configuration 79 | 80 | ## Build 81 | 82 | Docker build: 83 | 84 | ```bash 85 | docker build -t infomaniak/mcp-server-kdrive -f Dockerfile . 86 | ``` 87 | 88 | ## License 89 | 90 | This MCP server is licensed under the MIT License. 91 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import {McpServer, ResourceTemplate} from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import {z} from "zod"; 5 | import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js"; 6 | 7 | const token = process.env.KDRIVE_TOKEN; 8 | const drive_id = process.env.KDRIVE_ID; 9 | 10 | if (!token || !drive_id) { 11 | console.error( 12 | "Please set KDRIVE_TOKEN and KDRIVE_ID environment variables", 13 | ); 14 | process.exit(1); 15 | } 16 | 17 | const server = new McpServer( 18 | { 19 | name: "kDrive MCP Server", 20 | version: "0.0.1", 21 | }, 22 | { 23 | capabilities: { 24 | completions: {}, 25 | prompts: {}, 26 | resources: {}, 27 | tools: {}, 28 | }, 29 | }, 30 | ); 31 | 32 | class KdriveClient { 33 | private readonly headers: { Authorization: string; "Content-Type": string }; 34 | 35 | constructor() { 36 | this.headers = { 37 | Authorization: `Bearer ${token}`, 38 | "Content-Type": "application/json", 39 | }; 40 | } 41 | 42 | async search(query: string): Promise { 43 | const params = new URLSearchParams({ 44 | query, 45 | limit: "10" 46 | }); 47 | 48 | const response = await fetch( 49 | `https://api.infomaniak.com/3/drive/${drive_id}/files/search?${params}`, 50 | {headers: this.headers}, 51 | ); 52 | 53 | return response.json(); 54 | } 55 | 56 | async list(file_id = 1): Promise { 57 | const params = new URLSearchParams({ 58 | "type[]": "file" 59 | }); 60 | 61 | const response = await fetch( 62 | `https://api.infomaniak.com/3/drive/${drive_id}/files/${file_id}/files?${params}`, 63 | { 64 | headers: this.headers 65 | }, 66 | ); 67 | 68 | return response.json(); 69 | } 70 | 71 | async getFile(file_id: string): Promise { 72 | const response = await fetch( 73 | `https://api.infomaniak.com/2/drive/${drive_id}/files/${file_id}`, 74 | { 75 | headers: this.headers 76 | }, 77 | ); 78 | 79 | return response.json() 80 | } 81 | 82 | async downloadFile(file_id: string): Promise { 83 | const response = await fetch( 84 | `https://api.infomaniak.com/2/drive/${drive_id}/files/${file_id}/download`, 85 | { 86 | headers: this.headers 87 | }, 88 | ); 89 | 90 | return response.text() 91 | } 92 | } 93 | 94 | const kDriveClient = new KdriveClient(); 95 | 96 | server.tool( 97 | "kdrive_search", 98 | "Search for files in Infomanik kDrive", 99 | { 100 | query: z.string().describe("Search query") 101 | }, 102 | async ({query}) => { 103 | const response = await kDriveClient.search(query); 104 | 105 | return { 106 | content: [{type: "text", text: JSON.stringify(response)}], 107 | }; 108 | } 109 | ); 110 | 111 | server.resource( 112 | "kdrive_file", 113 | new ResourceTemplate("kdrive://{id}", { 114 | list: async function () { 115 | const rootFolder = await kDriveClient.list(); 116 | const response = await kDriveClient.list(rootFolder.data[0].id); 117 | 118 | return { 119 | resources: response.data.map((file: any) => ({ 120 | uri: `kdrive://${file.id}`, 121 | mimeType: file.mime_type, 122 | name: file.name, 123 | })), 124 | nextCursor: response.cursor 125 | } 126 | } 127 | }), 128 | async function (uri, datas) { 129 | const response = await kDriveClient.downloadFile(datas.id.toString()); 130 | const file = await kDriveClient.getFile(datas.id.toString()); 131 | 132 | return { 133 | contents: [{ 134 | uri: uri.href, 135 | blob: btoa(response), 136 | mimeType: file.data.mime_type 137 | }] 138 | }; 139 | } 140 | ); 141 | 142 | async function main() { 143 | const transport = new StdioServerTransport(); 144 | await server.connect(transport); 145 | } 146 | 147 | main().catch((error) => { 148 | console.error("Fatal error in main():", error); 149 | process.exit(1); 150 | }); 151 | --------------------------------------------------------------------------------