import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
import { google } from "googleapis";
import { promises as fs } from "fs";
import path from "path";
class GoogleDriveMCPServer {
constructor() {
this.server = new Server(
{
name: "google-drive-mcp-server",
version: "0.1.0",
},
{
capabilities: {
tools: {},
},
}
);
this.drive = null;
this.auth = null;
this.authType = null; // Track which auth method we're using
this.setupToolHandlers();
}
async initializeGoogleAuth() {
try {
console.error("Initializing Google authentication...");
// Use absolute paths - try multiple approaches to find the correct directory
let scriptDir;
try {
// Method 1: Using import.meta.url
scriptDir = path.dirname(new URL(import.meta.url).pathname);
} catch (e) {
// Method 2: Fallback to __dirname equivalent
scriptDir = path.dirname(process.argv[1]);
}
// If still not working, use the hardcoded path as absolute fallback
if (!scriptDir || scriptDir === '/') {
scriptDir = '/Users/vilhelm/Desktop/google-drive-mcp-server';
}
// Define paths for both auth methods
const serviceAccountPath = path.join(scriptDir, 'service-account.json');
const credentialsPath = path.join(scriptDir, 'credentials.json');
const tokenPath = path.join(scriptDir, 'token.json');
console.error("Script directory:", scriptDir);
// Try service account first, then fallback to OAuth
try {
console.error("Trying service account authentication...");
console.error("Looking for service account at:", serviceAccountPath);
const serviceAccountData = await fs.readFile(serviceAccountPath, 'utf8');
const serviceAccount = JSON.parse(serviceAccountData);
this.auth = new google.auth.GoogleAuth({
credentials: serviceAccount,
scopes: [
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive.readonly',
'https://www.googleapis.com/auth/drive.metadata.readonly'
]
});
this.authType = 'service-account';
console.error("✓ Service account authentication initialized");
console.error("Service account email:", serviceAccount.client_email);
} catch (serviceAccountError) {
// Fallback to OAuth authentication
console.error("Service account not found, falling back to OAuth...");
console.error("Service account error:", serviceAccountError.message);
console.error("Loading OAuth credentials from:", credentialsPath);
const credentials = JSON.parse(await fs.readFile(credentialsPath, 'utf8'));
const { client_secret, client_id, redirect_uris } = credentials.installed || credentials.web;
this.auth = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
console.error("✓ OAuth2 client created");
// Try to load existing token
try {
console.error("Loading OAuth token from:", tokenPath);
const token = JSON.parse(await fs.readFile(tokenPath, 'utf8'));
this.auth.setCredentials(token);
this.authType = 'oauth';
console.error("✓ OAuth token loaded and set");
} catch (tokenError) {
console.error("Error loading OAuth token:", tokenError.message);
throw new Error('OAuth authentication required. Run: node auth.js');
}
}
// Initialize Google Drive client
this.drive = google.drive({ version: 'v3', auth: this.auth });
console.error("✓ Google Drive client initialized");
console.error("Authentication method:", this.authType);
// Test the connection
try {
const testResponse = await this.drive.about.get({ fields: 'user' });
if (this.authType === 'oauth') {
console.error("✓ Connected as:", testResponse.data.user?.displayName || testResponse.data.user?.emailAddress);
} else {
console.error("✓ Service account connection verified");
}
} catch (testError) {
console.error("Warning: Could not verify connection:", testError.message);
}
} catch (error) {
console.error('Error initializing Google auth:', error);
throw error;
}
}
setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "list_files",
description: "List files in Google Drive. Can search by query, folder, or show shared files.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query (optional). Examples: 'name contains \"report\"', 'mimeType=\"application/pdf\"'",
},
maxResults: {
type: "number",
description: "Maximum number of results (default: 10)",
},
folderId: {
type: "string",
description: "Folder ID to search in (optional)",
},
includeShared: {
type: "boolean",
description: "Include files shared with me (default: false)",
},
},
},
},
{
name: "read_file",
description: "Read the contents of a file from Google Drive",
inputSchema: {
type: "object",
properties: {
fileId: {
type: "string",
description: "The ID of the file to read",
},
},
required: ["fileId"],
},
},
{
name: "create_file",
description: "Create a new file in Google Drive",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the file",
},
content: {
type: "string",
description: "Content of the file",
},
parentId: {
type: "string",
description: "Parent folder ID (optional)",
},
mimeType: {
type: "string",
description: "MIME type (default: text/plain)",
},
},
required: ["name", "content"],
},
},
{
name: "update_file",
description: "Update an existing file in Google Drive",
inputSchema: {
type: "object",
properties: {
fileId: {
type: "string",
description: "ID of the file to update",
},
content: {
type: "string",
description: "New content for the file",
},
},
required: ["fileId", "content"],
},
},
{
name: "delete_file",
description: "Delete a file from Google Drive",
inputSchema: {
type: "object",
properties: {
fileId: {
type: "string",
description: "ID of the file to delete",
},
},
required: ["fileId"],
},
},
{
name: "search_shared_files",
description: "Search for files shared with the authenticated account",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query (optional)",
},
maxResults: {
type: "number",
description: "Maximum number of results (default: 10)",
},
},
},
},
],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "list_files":
return await this.listFiles(args);
case "read_file":
return await this.readFile(args);
case "create_file":
return await this.createFile(args);
case "update_file":
return await this.updateFile(args);
case "delete_file":
return await this.deleteFile(args);
case "search_shared_files":
return await this.searchSharedFiles(args);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
} catch (error) {
console.error(`Error executing ${name}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Error executing ${name}: ${error.message}`
);
}
});
}
async listFiles(args) {
const { query = "", maxResults = 10, folderId, includeShared = false } = args;
let searchQuery = query;
// Build search query
const queryParts = [];
if (folderId) {
queryParts.push(`'${folderId}' in parents`);
}
if (includeShared) {
queryParts.push("sharedWithMe=true");
}
if (query) {
queryParts.push(query);
}
// Don't include trashed files by default
queryParts.push("trashed=false");
searchQuery = queryParts.join(" and ");
console.error("Search query:", searchQuery);
const response = await this.drive.files.list({
q: searchQuery,
pageSize: maxResults,
fields: 'files(id,name,mimeType,size,modifiedTime,parents,shared,owners)',
includeItemsFromAllDrives: true,
supportsAllDrives: true,
});
const files = response.data.files.map(file => ({
id: file.id,
name: file.name,
mimeType: file.mimeType,
size: file.size,
modifiedTime: file.modifiedTime,
shared: file.shared,
owners: file.owners?.map(owner => owner.emailAddress || owner.displayName)
}));
return {
content: [
{
type: "text",
text: `Found ${files.length} files:\n\n${JSON.stringify(files, null, 2)}`,
},
],
};
}
async searchSharedFiles(args) {
const { query = "", maxResults = 10 } = args;
let searchQuery = "sharedWithMe=true and trashed=false";
if (query) {
searchQuery += ` and ${query}`;
}
console.error("Shared files search query:", searchQuery);
const response = await this.drive.files.list({
q: searchQuery,
pageSize: maxResults,
fields: 'files(id,name,mimeType,size,modifiedTime,shared,owners,sharingUser)',
includeItemsFromAllDrives: true,
supportsAllDrives: true,
});
const files = response.data.files.map(file => ({
id: file.id,
name: file.name,
mimeType: file.mimeType,
size: file.size,
modifiedTime: file.modifiedTime,
owners: file.owners?.map(owner => owner.emailAddress || owner.displayName),
sharedBy: file.sharingUser?.emailAddress || file.sharingUser?.displayName
}));
return {
content: [
{
type: "text",
text: `Found ${files.length} shared files:\n\n${JSON.stringify(files, null, 2)}`,
},
],
};
}
async readFile(args) {
const { fileId } = args;
// Get file metadata first
const metadata = await this.drive.files.get({
fileId: fileId,
fields: 'name,mimeType,size,owners',
supportsAllDrives: true,
});
// Handle different file types
let content = "";
const mimeType = metadata.data.mimeType;
try {
if (mimeType.includes('google-apps')) {
// Handle Google Workspace files (Docs, Sheets, etc.)
let exportMimeType = 'text/plain';
if (mimeType.includes('document')) {
exportMimeType = 'text/plain';
} else if (mimeType.includes('spreadsheet')) {
exportMimeType = 'text/csv';
} else if (mimeType.includes('presentation')) {
exportMimeType = 'text/plain';
}
const response = await this.drive.files.export({
fileId: fileId,
mimeType: exportMimeType,
});
content = response.data;
} else {
// Handle regular files
const response = await this.drive.files.get({
fileId: fileId,
alt: 'media',
supportsAllDrives: true,
});
content = response.data;
}
} catch (error) {
content = `Error reading file content: ${error.message}`;
}
return {
content: [
{
type: "text",
text: `File: ${metadata.data.name}
MIME Type: ${metadata.data.mimeType}
Size: ${metadata.data.size || 'N/A'} bytes
Owners: ${metadata.data.owners?.map(o => o.emailAddress).join(', ') || 'Unknown'}
Content:
${content}`,
},
],
};
}
async createFile(args) {
const { name, content, parentId, mimeType = 'text/plain' } = args;
const fileMetadata = {
name: name,
parents: parentId ? [parentId] : undefined,
};
const media = {
mimeType: mimeType,
body: content,
};
const response = await this.drive.files.create({
requestBody: fileMetadata,
media: media,
fields: 'id,name,parents',
supportsAllDrives: true,
});
return {
content: [
{
type: "text",
text: `File created successfully!
File ID: ${response.data.id}
Name: ${response.data.name}
Parent folder: ${response.data.parents?.[0] || 'Root'}
Authentication method used: ${this.authType}`,
},
],
};
}
async updateFile(args) {
const { fileId, content } = args;
const media = {
mimeType: 'text/plain',
body: content,
};
const response = await this.drive.files.update({
fileId: fileId,
media: media,
fields: 'id,name,modifiedTime',
supportsAllDrives: true,
});
return {
content: [
{
type: "text",
text: `File updated successfully!
File ID: ${response.data.id}
Name: ${response.data.name}
Modified: ${response.data.modifiedTime}
Authentication method used: ${this.authType}`,
},
],
};
}
async deleteFile(args) {
const { fileId } = args;
// Get file name before deleting
const metadata = await this.drive.files.get({
fileId: fileId,
fields: 'name',
supportsAllDrives: true,
});
await this.drive.files.delete({
fileId: fileId,
supportsAllDrives: true,
});
return {
content: [
{
type: "text",
text: `File "${metadata.data.name}" (ID: ${fileId}) deleted successfully!
Authentication method used: ${this.authType}`,
},
],
};
}
async run() {
try {
console.error("Starting Google Drive MCP server...");
await this.initializeGoogleAuth();
console.error("✓ Google authentication initialized");
const transport = new StdioServerTransport();
console.error("✓ Transport created");
await this.server.connect(transport);
console.error("✓ Server connected successfully");
// Keep the process alive
process.on('SIGINT', () => {
console.error("Received SIGINT, shutting down gracefully...");
process.exit(0);
});
process.on('SIGTERM', () => {
console.error("Received SIGTERM, shutting down gracefully...");
process.exit(0);
});
} catch (error) {
console.error("Failed to start server:", error);
console.error("Error details:", error.stack);
process.exit(1);
}
}
}
// Start the server
const server = new GoogleDriveMCPServer();
server.run().catch(console.error);