# Directory Structure
```
├── Dockerfile
├── index.ts
├── package.json
├── README.md
├── replace_open.sh
├── tsconfig.base.json
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Google Drive server
2 |
3 | This MCP server integrates with Google Drive to allow listing, reading, and searching over files.
4 |
5 | ## Components
6 |
7 | ### Tools
8 |
9 | - **search**
10 | - Search for files in Google Drive
11 | - Input: `query` (string): Search query
12 | - Returns file names and MIME types of matching files
13 |
14 | ### Resources
15 |
16 | The server provides access to Google Drive files:
17 |
18 | - **Files** (`gdrive:///<file_id>`)
19 | - Supports all file types
20 | - Google Workspace files are automatically exported:
21 | - Docs → Markdown
22 | - Sheets → CSV
23 | - Presentations → Plain text
24 | - Drawings → PNG
25 | - Other files are provided in their native format
26 |
27 | ## Getting started
28 |
29 | 1. [Create a new Google Cloud project](https://console.cloud.google.com/projectcreate)
30 | 2. [Enable the Google Drive API](https://console.cloud.google.com/workspace-api/products)
31 | 3. [Configure an OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) ("internal" is fine for testing)
32 | 4. Add OAuth scope `https://www.googleapis.com/auth/drive.readonly`
33 | 5. [Create an OAuth Client ID](https://console.cloud.google.com/apis/credentials/oauthclient) for application type "Desktop App"
34 | 6. Download the JSON file of your client's OAuth keys
35 | 7. Rename the key file to `gcp-oauth.keys.json` and place into the root of this repo (i.e. `servers/gcp-oauth.keys.json`)
36 |
37 | Make sure to build the server with either `npm run build` or `npm run watch`.
38 |
39 | ### Authentication
40 |
41 | To authenticate and save credentials:
42 |
43 | 1. Run the server with the `auth` argument: `node ./dist auth`
44 | 2. This will open an authentication flow in your system browser
45 | 3. Complete the authentication process
46 | 4. Credentials will be saved in the root of this repo (i.e. `servers/.gdrive-server-credentials.json`)
47 |
48 | ### Usage with Desktop App
49 |
50 | To integrate this server with the desktop app, add the following to your app's server configuration:
51 |
52 | #### Docker
53 |
54 | Authentication:
55 |
56 | Assuming you have completed setting up the OAuth application on Google Cloud, you can now auth the server with the following command, replacing `/path/to/gcp-oauth.keys.json` with the path to your OAuth keys file:
57 |
58 | ```bash
59 | docker run -i --rm --mount type=bind,source=/path/to/gcp-oauth.keys.json,target=/gcp-oauth.keys.json -v mcp-gdrive:/gdrive-server -e GDRIVE_OAUTH_PATH=/gcp-oauth.keys.json -e "GDRIVE_CREDENTIALS_PATH=/gdrive-server/credentials.json" -p 3000:3000 mcp/gdrive auth
60 | ```
61 |
62 | The command will print the URL to open in your browser. Open this URL in your browser and complete the authentication process. The credentials will be saved in the `mcp-gdrive` volume.
63 |
64 | Once authenticated, you can use the server in your app's server configuration:
65 |
66 | ```json
67 | {
68 | "mcpServers": {
69 | "gdrive": {
70 | "command": "docker",
71 | "args": ["run", "-i", "--rm", "-v", "mcp-gdrive:/gdrive-server", "-e", "GDRIVE_CREDENTIALS_PATH=/gdrive-server/credentials.json", "mcp/gdrive"]
72 | }
73 | }
74 | }
75 | ```
76 |
77 | #### NPX
78 |
79 | ```json
80 | {
81 | "mcpServers": {
82 | "gdrive": {
83 | "command": "npx",
84 | "args": [
85 | "-y",
86 | "@modelcontextprotocol/server-gdrive"
87 | ]
88 | }
89 | }
90 | }
91 | ```
92 |
93 | ## License
94 |
95 | 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.
96 |
```
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "forceConsistentCasingInFileNames": true
10 | }
11 | }
12 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "./dist",
5 | "rootDir": ".",
6 | "target": "ES2022",
7 | "module": "NodeNext",
8 | "moduleResolution": "NodeNext",
9 | "esModuleInterop": true,
10 | "allowSyntheticDefaultImports": true
11 | },
12 | "include": [
13 | "./**/*.ts"
14 | ]
15 | }
16 |
```
--------------------------------------------------------------------------------
/replace_open.sh:
--------------------------------------------------------------------------------
```bash
1 | #! /bin/bash
2 |
3 | # Basic script to replace opn(authorizeUrl, { wait: false }).then(cp => cp.unref()); with process.stdout.write(`Open this URL in your browser: ${authorizeUrl}`);
4 |
5 | sed -i 's/opn(authorizeUrl, { wait: false }).then(cp => cp.unref());/process.stderr.write(`Open this URL in your browser: ${authorizeUrl}\n`);/' node_modules/@google-cloud/local-auth/build/src/index.js
6 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@modelcontextprotocol/server-gdrive",
3 | "version": "0.6.2",
4 | "description": "MCP server for interacting with Google Drive",
5 | "license": "MIT",
6 | "author": "Anthropic, PBC (https://anthropic.com)",
7 | "homepage": "https://modelcontextprotocol.io",
8 | "bugs": "https://github.com/modelcontextprotocol/servers/issues",
9 | "type": "module",
10 | "bin": {
11 | "mcp-server-gdrive": "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 | "@google-cloud/local-auth": "^3.0.1",
23 | "@modelcontextprotocol/sdk": "1.0.1",
24 | "googleapis": "^144.0.0"
25 | },
26 | "devDependencies": {
27 | "@types/node": "^22",
28 | "shx": "^0.3.4",
29 | "typescript": "^5.6.2"
30 | }
31 | }
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Stage 1: Builder
2 | FROM node:22.12-alpine AS builder
3 |
4 | # Set working directory
5 | WORKDIR /app
6 |
7 | # Copy necessary files
8 | COPY . .
9 | COPY tsconfig.json tsconfig.json
10 |
11 | # Install dependencies with better compatibility
12 | RUN --mount=type=cache,target=/root/.npm npm install --legacy-peer-deps
13 |
14 | # Ensure TypeScript builds successfully
15 | RUN npx tsc --noEmit
16 |
17 | # Build TypeScript files
18 | RUN npm run build
19 |
20 | # Stage 2: Release
21 | FROM node:22-alpine AS release
22 |
23 | # Set working directory
24 | WORKDIR /app
25 |
26 | # Copy built files and package files
27 | COPY --from=builder /app/dist /app/dist
28 | COPY --from=builder /app/package.json /app/package.json
29 | COPY --from=builder /app/package-lock.json /app/package-lock.json
30 | COPY replace_open.sh /replace_open.sh
31 |
32 | # Set environment variable
33 | ENV NODE_ENV=production
34 |
35 | # Install only production dependencies
36 | RUN npm ci --ignore-scripts --omit-dev
37 |
38 | # Run the script and clean up
39 | RUN sh /replace_open.sh && rm /replace_open.sh
40 |
41 | # Define the entry point
42 | ENTRYPOINT ["node", "dist/index.js"]
43 |
```
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { authenticate } from "@google-cloud/local-auth";
4 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6 | import {
7 | CallToolRequestSchema,
8 | ListResourcesRequestSchema,
9 | ListToolsRequestSchema,
10 | ReadResourceRequestSchema,
11 | } from "@modelcontextprotocol/sdk/types.js";
12 | import fs from "fs";
13 | import { google } from "googleapis";
14 | import path from "path";
15 |
16 | const drive = google.drive("v3");
17 |
18 | const server = new Server(
19 | {
20 | name: "example-servers/gdrive",
21 | version: "0.1.0",
22 | },
23 | {
24 | capabilities: {
25 | resources: {},
26 | tools: {},
27 | },
28 | },
29 | );
30 |
31 | server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
32 | const pageSize = 10;
33 | const params: any = {
34 | pageSize,
35 | fields: "nextPageToken, files(id, name, mimeType)",
36 | };
37 |
38 | if (request.params?.cursor) {
39 | params.pageToken = request.params.cursor;
40 | }
41 |
42 | const res = await drive.files.list(params);
43 | const files = res.data.files!;
44 |
45 | return {
46 | resources: files.map((file) => ({
47 | uri: `gdrive:///${file.id}`,
48 | mimeType: file.mimeType,
49 | name: file.name,
50 | })),
51 | nextCursor: res.data.nextPageToken,
52 | };
53 | });
54 |
55 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
56 | const fileId = request.params.uri.replace("gdrive:///", "");
57 |
58 | // First get file metadata to check mime type
59 | const file = await drive.files.get({
60 | fileId,
61 | fields: "mimeType",
62 | });
63 |
64 | // For Google Docs/Sheets/etc we need to export
65 | if (file.data.mimeType?.startsWith("application/vnd.google-apps")) {
66 | let exportMimeType: string;
67 | switch (file.data.mimeType) {
68 | case "application/vnd.google-apps.document":
69 | exportMimeType = "text/markdown";
70 | break;
71 | case "application/vnd.google-apps.spreadsheet":
72 | exportMimeType = "text/csv";
73 | break;
74 | case "application/vnd.google-apps.presentation":
75 | exportMimeType = "text/plain";
76 | break;
77 | case "application/vnd.google-apps.drawing":
78 | exportMimeType = "image/png";
79 | break;
80 | default:
81 | exportMimeType = "text/plain";
82 | }
83 |
84 | const res = await drive.files.export(
85 | { fileId, mimeType: exportMimeType },
86 | { responseType: "text" },
87 | );
88 |
89 | return {
90 | contents: [
91 | {
92 | uri: request.params.uri,
93 | mimeType: exportMimeType,
94 | text: res.data,
95 | },
96 | ],
97 | };
98 | }
99 |
100 | // For regular files download content
101 | const res = await drive.files.get(
102 | { fileId, alt: "media" },
103 | { responseType: "arraybuffer" },
104 | );
105 | const mimeType = file.data.mimeType || "application/octet-stream";
106 | if (mimeType.startsWith("text/") || mimeType === "application/json") {
107 | return {
108 | contents: [
109 | {
110 | uri: request.params.uri,
111 | mimeType: mimeType,
112 | text: Buffer.from(res.data as ArrayBuffer).toString("utf-8"),
113 | },
114 | ],
115 | };
116 | } else {
117 | return {
118 | contents: [
119 | {
120 | uri: request.params.uri,
121 | mimeType: mimeType,
122 | blob: Buffer.from(res.data as ArrayBuffer).toString("base64"),
123 | },
124 | ],
125 | };
126 | }
127 | });
128 |
129 | server.setRequestHandler(ListToolsRequestSchema, async () => {
130 | return {
131 | tools: [
132 | {
133 | name: "search",
134 | description: "Search for files in Google Drive",
135 | inputSchema: {
136 | type: "object",
137 | properties: {
138 | query: {
139 | type: "string",
140 | description: "Search query",
141 | },
142 | },
143 | required: ["query"],
144 | },
145 | },
146 | ],
147 | };
148 | });
149 |
150 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
151 | if (request.params.name === "search") {
152 | const userQuery = request.params.arguments?.query as string;
153 | const escapedQuery = userQuery.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
154 | const formattedQuery = `fullText contains '${escapedQuery}'`;
155 |
156 | const res = await drive.files.list({
157 | q: formattedQuery,
158 | pageSize: 10,
159 | fields: "files(id, name, mimeType, modifiedTime, size)",
160 | });
161 |
162 | const fileList = res.data.files
163 | ?.map((file: any) => `${file.name} (${file.mimeType})`)
164 | .join("\n");
165 | return {
166 | content: [
167 | {
168 | type: "text",
169 | text: `Found ${res.data.files?.length ?? 0} files:\n${fileList}`,
170 | },
171 | ],
172 | isError: false,
173 | };
174 | }
175 | throw new Error("Tool not found");
176 | });
177 |
178 | const credentialsPath = process.env.GDRIVE_CREDENTIALS_PATH || path.join(
179 | path.dirname(new URL(import.meta.url).pathname),
180 | "../../../.gdrive-server-credentials.json",
181 | );
182 |
183 | async function authenticateAndSaveCredentials() {
184 | console.log("Launching auth flow…");
185 | const auth = await authenticate({
186 | keyfilePath: process.env.GDRIVE_OAUTH_PATH || path.join(
187 | path.dirname(new URL(import.meta.url).pathname),
188 | "../../../gcp-oauth.keys.json",
189 | ),
190 | scopes: ["https://www.googleapis.com/auth/drive.readonly"],
191 | });
192 | fs.writeFileSync(credentialsPath, JSON.stringify(auth.credentials));
193 | console.log("Credentials saved. You can now run the server.");
194 | }
195 |
196 | async function loadCredentialsAndRunServer() {
197 | if (!fs.existsSync(credentialsPath)) {
198 | console.error(
199 | "Credentials not found. Please run with 'auth' argument first.",
200 | );
201 | process.exit(1);
202 | }
203 |
204 | const credentials = JSON.parse(fs.readFileSync(credentialsPath, "utf-8"));
205 | const auth = new google.auth.OAuth2();
206 | auth.setCredentials(credentials);
207 | google.options({ auth });
208 |
209 | console.error("Credentials loaded. Starting server.");
210 | const transport = new StdioServerTransport();
211 | await server.connect(transport);
212 | }
213 |
214 | if (process.argv[2] === "auth") {
215 | authenticateAndSaveCredentials().catch(console.error);
216 | } else {
217 | loadCredentialsAndRunServer().catch(console.error);
218 | }
219 |
```