@piotr-agier/google-drive-mcp
Version:
Google Drive MCP Server - Model Context Protocol server providing secure access to Google Drive, Docs, Sheets, and Slides through MCP clients e.g. Claude Desktop
1,377 lines (1,363 loc) • 134 kB
JavaScript
#!/usr/bin/env node
// src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema
} from "@modelcontextprotocol/sdk/types.js";
import { google } from "googleapis";
import { v4 as uuidv4 } from "uuid";
// src/auth/client.ts
import { OAuth2Client } from "google-auth-library";
import * as fs from "fs/promises";
// src/auth/utils.ts
import * as path from "path";
import * as os from "os";
import { fileURLToPath } from "url";
function getProjectRoot() {
const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.join(__dirname2, "..", "..");
return path.resolve(projectRoot);
}
function getSecureTokenPath() {
const customTokenPath = process.env.GOOGLE_DRIVE_MCP_TOKEN_PATH;
if (customTokenPath) {
return path.resolve(customTokenPath);
}
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
const tokenDir = path.join(configHome, "google-drive-mcp");
return path.join(tokenDir, "tokens.json");
}
function getLegacyTokenPath() {
const projectRoot = getProjectRoot();
return path.join(projectRoot, ".gcp-saved-tokens.json");
}
function getAdditionalLegacyPaths() {
return [
process.env.GOOGLE_TOKEN_PATH,
path.join(process.cwd(), "google-tokens.json"),
path.join(process.cwd(), ".gcp-saved-tokens.json")
].filter(Boolean);
}
function getKeysFilePath() {
const envCredentialsPath = process.env.GOOGLE_DRIVE_OAUTH_CREDENTIALS;
if (envCredentialsPath) {
return path.resolve(envCredentialsPath);
}
const projectRoot = getProjectRoot();
const keysPath = path.join(projectRoot, "gcp-oauth.keys.json");
return keysPath;
}
function generateCredentialsErrorMessage() {
return `
OAuth credentials not found. Please provide credentials using one of these methods:
1. Environment variable:
Set GOOGLE_DRIVE_OAUTH_CREDENTIALS to the path of your credentials file:
export GOOGLE_DRIVE_OAUTH_CREDENTIALS="/path/to/gcp-oauth.keys.json"
2. Default file path:
Place your gcp-oauth.keys.json file in the package root directory.
Token storage:
- Tokens are saved to: ${getSecureTokenPath()}
- To use a custom token location, set GOOGLE_DRIVE_MCP_TOKEN_PATH environment variable
To get OAuth credentials:
1. Go to the Google Cloud Console (https://console.cloud.google.com/)
2. Create or select a project
3. Enable the Google Drive, Docs, Sheets, and Slides APIs
4. Create OAuth 2.0 credentials (Desktop app type)
5. Download the credentials file as gcp-oauth.keys.json
`.trim();
}
// src/auth/client.ts
async function loadCredentialsFromFile() {
const keysContent = await fs.readFile(getKeysFilePath(), "utf-8");
const keys = JSON.parse(keysContent);
if (keys.installed) {
const { client_id, client_secret, redirect_uris } = keys.installed;
return { client_id, client_secret, redirect_uris };
} else if (keys.web) {
const { client_id, client_secret, redirect_uris } = keys.web;
return { client_id, client_secret, redirect_uris };
} else if (keys.client_id) {
return {
client_id: keys.client_id,
client_secret: keys.client_secret,
redirect_uris: keys.redirect_uris || ["http://localhost:3000/oauth2callback"]
};
} else {
throw new Error('Invalid credentials file format. Expected either "installed", "web" object or direct client_id field.');
}
}
async function loadCredentialsWithFallback() {
try {
return await loadCredentialsFromFile();
} catch (fileError) {
const legacyPath = process.env.GOOGLE_CLIENT_SECRET_PATH || "client_secret.json";
try {
const legacyContent = await fs.readFile(legacyPath, "utf-8");
const legacyKeys = JSON.parse(legacyContent);
console.error("Warning: Using legacy client_secret.json. Please migrate to gcp-oauth.keys.json");
if (legacyKeys.installed) {
return legacyKeys.installed;
} else if (legacyKeys.web) {
return legacyKeys.web;
} else {
throw new Error("Invalid legacy credentials format");
}
} catch (legacyError) {
const errorMessage = generateCredentialsErrorMessage();
throw new Error(`${errorMessage}
Original error: ${fileError instanceof Error ? fileError.message : fileError}`);
}
}
}
async function initializeOAuth2Client() {
try {
const credentials = await loadCredentialsWithFallback();
return new OAuth2Client({
clientId: credentials.client_id,
clientSecret: credentials.client_secret || void 0,
redirectUri: credentials.redirect_uris?.[0] || "http://localhost:3000/oauth2callback"
});
} catch (error) {
throw new Error(`Error loading OAuth keys: ${error instanceof Error ? error.message : error}`);
}
}
async function loadCredentials() {
try {
const credentials = await loadCredentialsWithFallback();
if (!credentials.client_id) {
throw new Error("Client ID missing in credentials.");
}
return {
client_id: credentials.client_id,
client_secret: credentials.client_secret
};
} catch (error) {
throw new Error(`Error loading credentials: ${error instanceof Error ? error.message : error}`);
}
}
// src/auth/server.ts
import express from "express";
import { OAuth2Client as OAuth2Client2 } from "google-auth-library";
// src/auth/tokenManager.ts
import * as fs2 from "fs/promises";
import * as path2 from "path";
import { GaxiosError } from "gaxios";
var TokenManager = class {
constructor(oauth2Client) {
this.oauth2Client = oauth2Client;
this.tokenPath = getSecureTokenPath();
this.setupTokenRefresh();
}
// Method to expose the token path
getTokenPath() {
return this.tokenPath;
}
async ensureTokenDirectoryExists() {
try {
const dir = path2.dirname(this.tokenPath);
await fs2.mkdir(dir, { recursive: true });
} catch (error) {
if (error instanceof Error && "code" in error && error.code !== "EEXIST") {
console.error("Failed to create token directory:", error);
throw error;
}
}
}
setupTokenRefresh() {
this.oauth2Client.on("tokens", async (newTokens) => {
try {
await this.ensureTokenDirectoryExists();
const currentTokens = JSON.parse(await fs2.readFile(this.tokenPath, "utf-8"));
const updatedTokens = {
...currentTokens,
...newTokens,
refresh_token: newTokens.refresh_token || currentTokens.refresh_token
};
await fs2.writeFile(this.tokenPath, JSON.stringify(updatedTokens, null, 2), {
mode: 384
});
console.error("Tokens updated and saved");
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
try {
await fs2.writeFile(this.tokenPath, JSON.stringify(newTokens, null, 2), { mode: 384 });
console.error("New tokens saved");
} catch (writeError) {
console.error("Error saving initial tokens:", writeError);
}
} else {
console.error("Error saving updated tokens:", error);
}
}
});
}
async migrateLegacyTokens() {
const legacyPaths = [getLegacyTokenPath(), ...getAdditionalLegacyPaths()];
for (const legacyPath of legacyPaths) {
try {
if (!await fs2.access(legacyPath).then(() => true).catch(() => false)) {
continue;
}
const legacyTokens = JSON.parse(await fs2.readFile(legacyPath, "utf-8"));
if (!legacyTokens || typeof legacyTokens !== "object") {
console.error("Invalid legacy token format at", legacyPath, ", skipping");
continue;
}
await this.ensureTokenDirectoryExists();
await fs2.writeFile(this.tokenPath, JSON.stringify(legacyTokens, null, 2), {
mode: 384
});
console.error("Migrated tokens from legacy location:", legacyPath, "to:", this.tokenPath);
try {
await fs2.unlink(legacyPath);
console.error("Removed legacy token file");
} catch (unlinkErr) {
console.error("Warning: Could not remove legacy token file:", unlinkErr);
}
return true;
} catch (error) {
console.error("Error migrating legacy tokens from", legacyPath, ":", error);
}
}
return false;
}
async loadSavedTokens() {
try {
await this.ensureTokenDirectoryExists();
const tokenExists = await fs2.access(this.tokenPath).then(() => true).catch(() => false);
if (!tokenExists) {
const migrated = await this.migrateLegacyTokens();
if (!migrated) {
console.error("No token file found at:", this.tokenPath);
return false;
}
}
const tokens = JSON.parse(await fs2.readFile(this.tokenPath, "utf-8"));
if (!tokens || typeof tokens !== "object") {
console.error("Invalid token format in file:", this.tokenPath);
return false;
}
this.oauth2Client.setCredentials(tokens);
console.error("Tokens loaded and set on OAuth2Client:", {
hasAccessToken: !!tokens.access_token,
hasRefreshToken: !!tokens.refresh_token,
tokenLength: tokens.access_token?.length,
expiryDate: tokens.expiry_date,
scope: tokens.scope
});
console.error("OAuth2Client after setCredentials:", {
hasCredentials: !!this.oauth2Client.credentials,
credentialsAccessToken: !!this.oauth2Client.credentials?.access_token
});
return true;
} catch (error) {
console.error("Error loading tokens:", error);
if (error instanceof Error && "code" in error && error.code !== "ENOENT") {
try {
await fs2.unlink(this.tokenPath);
console.error("Removed potentially corrupted token file");
} catch (unlinkErr) {
}
}
return false;
}
}
async refreshTokensIfNeeded() {
const expiryDate = this.oauth2Client.credentials.expiry_date;
const isExpired = expiryDate ? Date.now() >= expiryDate - 5 * 60 * 1e3 : !this.oauth2Client.credentials.access_token;
if (isExpired && this.oauth2Client.credentials.refresh_token) {
console.error("Auth token expired or nearing expiry, refreshing...");
try {
const response = await this.oauth2Client.refreshAccessToken();
const newTokens = response.credentials;
if (!newTokens.access_token) {
throw new Error("Received invalid tokens during refresh");
}
this.oauth2Client.setCredentials(newTokens);
console.error("Token refreshed successfully");
return true;
} catch (refreshError) {
if (refreshError instanceof GaxiosError && refreshError.response?.data?.error === "invalid_grant") {
console.error("Error refreshing auth token: Invalid grant. Token likely expired or revoked. Please re-authenticate.");
await this.clearTokens();
return false;
} else {
console.error("Error refreshing auth token:", refreshError);
return false;
}
}
} else if (!this.oauth2Client.credentials.access_token && !this.oauth2Client.credentials.refresh_token) {
console.error("No access or refresh token available. Please re-authenticate.");
return false;
} else {
return true;
}
}
async validateTokens() {
if (!this.oauth2Client.credentials || !this.oauth2Client.credentials.access_token) {
if (!await this.loadSavedTokens()) {
return false;
}
if (!this.oauth2Client.credentials || !this.oauth2Client.credentials.access_token) {
return false;
}
}
return this.refreshTokensIfNeeded();
}
async saveTokens(tokens) {
try {
await this.ensureTokenDirectoryExists();
await fs2.writeFile(this.tokenPath, JSON.stringify(tokens, null, 2), { mode: 384 });
this.oauth2Client.setCredentials(tokens);
console.error("Tokens saved successfully to:", this.tokenPath);
} catch (error) {
console.error("Error saving tokens:", error);
throw error;
}
}
async clearTokens() {
try {
this.oauth2Client.setCredentials({});
await fs2.unlink(this.tokenPath);
console.error("Tokens cleared successfully");
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
console.error("Token file already deleted");
} else {
console.error("Error clearing tokens:", error);
}
}
}
};
// src/auth/server.ts
import open from "open";
var 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/documents",
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/presentations"
];
var AuthServer = class {
// Flag for standalone script
constructor(oauth2Client) {
// Used by TokenManager for validation/refresh
this.flowOAuth2Client = null;
this.server = null;
this.authCompletedSuccessfully = false;
this.baseOAuth2Client = oauth2Client;
this.tokenManager = new TokenManager(oauth2Client);
this.app = express();
this.portRange = { start: 3e3, end: 3004 };
this.setupRoutes();
}
setupRoutes() {
this.app.get("/", (req, res) => {
const clientForUrl = this.flowOAuth2Client || this.baseOAuth2Client;
const authUrl = clientForUrl.generateAuthUrl({
access_type: "offline",
scope: SCOPES,
prompt: "consent"
});
res.send(`<h1>Google Drive Authentication</h1><a href="${authUrl}">Authenticate with Google</a>`);
});
this.app.get("/oauth2callback", async (req, res) => {
const code = req.query.code;
if (!code) {
res.status(400).send("Authorization code missing");
return;
}
if (!this.flowOAuth2Client) {
res.status(500).send("Authentication flow not properly initiated.");
return;
}
try {
const { tokens } = await this.flowOAuth2Client.getToken(code);
await this.tokenManager.saveTokens(tokens);
this.authCompletedSuccessfully = true;
const tokenPath = this.tokenManager.getTokenPath();
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Authentication Successful</title>
<style>
body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f4f4f4; margin: 0; }
.container { text-align: center; padding: 2em; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
h1 { color: #4CAF50; }
p { color: #333; margin-bottom: 0.5em; }
code { background-color: #eee; padding: 0.2em 0.4em; border-radius: 3px; font-size: 0.9em; }
</style>
</head>
<body>
<div class="container">
<h1>Authentication Successful!</h1>
<p>Your authentication tokens have been saved successfully to:</p>
<p><code>${tokenPath}</code></p>
<p>You can now close this browser window.</p>
</div>
</body>
</html>
`);
} catch (error) {
this.authCompletedSuccessfully = false;
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Authentication Failed</title>
<style>
body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f4f4f4; margin: 0; }
.container { text-align: center; padding: 2em; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
h1 { color: #F44336; }
p { color: #333; }
</style>
</head>
<body>
<div class="container">
<h1>Authentication Failed</h1>
<p>An error occurred during authentication:</p>
<p><code>${message}</code></p>
<p>Please try again or check the server logs.</p>
</div>
</body>
</html>
`);
}
});
}
async start(openBrowser = true) {
if (await this.tokenManager.validateTokens()) {
this.authCompletedSuccessfully = true;
return true;
}
const port = await this.startServerOnAvailablePort();
if (port === null) {
this.authCompletedSuccessfully = false;
return false;
}
try {
const { client_id, client_secret } = await loadCredentials();
this.flowOAuth2Client = new OAuth2Client2(
client_id,
client_secret || void 0,
`http://localhost:${port}/oauth2callback`
);
} catch (error) {
console.error("Failed to load credentials for auth flow:", error);
this.authCompletedSuccessfully = false;
await this.stop();
return false;
}
if (openBrowser) {
const authorizeUrl = this.flowOAuth2Client.generateAuthUrl({
access_type: "offline",
scope: SCOPES,
prompt: "consent"
});
console.error("\n\u{1F510} AUTHENTICATION REQUIRED");
console.error("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
console.error("\nOpening your browser to authenticate...");
console.error(`If the browser doesn't open, visit:
${authorizeUrl}
`);
await open(authorizeUrl);
}
return true;
}
async startServerOnAvailablePort() {
for (let port = this.portRange.start; port <= this.portRange.end; port++) {
try {
await new Promise((resolve2, reject) => {
const testServer = this.app.listen(port, () => {
this.server = testServer;
console.error(`Authentication server listening on http://localhost:${port}`);
resolve2();
});
testServer.on("error", (err) => {
if (err.code === "EADDRINUSE") {
testServer.close(() => reject(err));
} else {
reject(err);
}
});
});
return port;
} catch (error) {
if (!(error instanceof Error && "code" in error && error.code === "EADDRINUSE")) {
console.error("Failed to start auth server:", error);
return null;
}
}
}
console.error("No available ports for authentication server (tried ports", this.portRange.start, "-", this.portRange.end, ")");
return null;
}
getRunningPort() {
if (this.server) {
const address = this.server.address();
if (typeof address === "object" && address !== null) {
return address.port;
}
}
return null;
}
async stop() {
return new Promise((resolve2, reject) => {
if (this.server) {
this.server.close((err) => {
if (err) {
reject(err);
} else {
this.server = null;
resolve2();
}
});
} else {
resolve2();
}
});
}
};
// src/auth.ts
async function authenticate() {
console.error("Initializing authentication...");
const oauth2Client = await initializeOAuth2Client();
const tokenManager = new TokenManager(oauth2Client);
if (await tokenManager.validateTokens()) {
console.error("Authentication successful - using existing tokens");
console.error("OAuth2Client credentials:", {
hasAccessToken: !!oauth2Client.credentials?.access_token,
hasRefreshToken: !!oauth2Client.credentials?.refresh_token,
expiryDate: oauth2Client.credentials?.expiry_date
});
return oauth2Client;
}
console.error("\n\u{1F510} No valid authentication tokens found.");
console.error("Starting authentication flow...\n");
const authServer = new AuthServer(oauth2Client);
const authSuccess = await authServer.start(true);
if (!authSuccess) {
throw new Error("Authentication failed. Please check your credentials and try again.");
}
await new Promise((resolve2) => {
const checkInterval = setInterval(async () => {
if (authServer.authCompletedSuccessfully) {
clearInterval(checkInterval);
await authServer.stop();
resolve2();
}
}, 1e3);
});
return oauth2Client;
}
// src/index.ts
import { z } from "zod";
import { fileURLToPath as fileURLToPath2 } from "url";
import { readFileSync } from "fs";
import { join as join2, dirname as dirname3 } from "path";
var drive = null;
function ensureDriveService() {
if (!authClient) {
throw new Error("Authentication required");
}
log("About to create drive service", {
authClientType: authClient?.constructor?.name,
hasCredentials: !!authClient.credentials,
credentialsKeys: authClient.credentials ? Object.keys(authClient.credentials) : [],
accessTokenLength: authClient.credentials?.access_token?.length,
accessTokenPrefix: authClient.credentials?.access_token?.substring(0, 20),
expiryDate: authClient.credentials?.expiry_date,
isExpired: authClient.credentials?.expiry_date ? Date.now() > authClient.credentials.expiry_date : "no expiry"
});
drive = google.drive({ version: "v3", auth: authClient });
log("Drive service created/updated", {
hasAuth: !!authClient,
hasCredentials: !!authClient.credentials,
hasAccessToken: !!authClient.credentials?.access_token
});
drive.about.get({ fields: "user" }).then((response) => {
log("Auth test successful, user:", response.data.user?.emailAddress);
}).catch((error) => {
log("Auth test failed:", error.message || error);
if (error.response) {
log("Auth test error details:", {
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
data: error.response.data
});
}
});
}
var FOLDER_MIME_TYPE = "application/vnd.google-apps.folder";
var TEXT_MIME_TYPES = {
txt: "text/plain",
md: "text/markdown"
};
var authClient = null;
var authenticationPromise = null;
var __filename = fileURLToPath2(import.meta.url);
var __dirname = dirname3(__filename);
var packageJsonPath = join2(__dirname, "..", "package.json");
var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
var VERSION = packageJson.version;
function log(message, data) {
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
const logMessage = data ? `[${timestamp}] ${message}: ${JSON.stringify(data)}` : `[${timestamp}] ${message}`;
console.error(logMessage);
}
function getExtensionFromFilename(filename) {
return filename.split(".").pop()?.toLowerCase() || "";
}
function getMimeTypeFromFilename(filename) {
const ext = getExtensionFromFilename(filename);
return TEXT_MIME_TYPES[ext] || "text/plain";
}
async function resolvePath(pathStr) {
if (!pathStr || pathStr === "/") return "root";
const parts = pathStr.replace(/^\/+|\/+$/g, "").split("/");
let currentFolderId = "root";
for (const part of parts) {
if (!part) continue;
let response = await drive.files.list({
q: `'${currentFolderId}' in parents and name = '${part}' and mimeType = '${FOLDER_MIME_TYPE}' and trashed = false`,
fields: "files(id)",
spaces: "drive"
});
if (!response.data.files?.length) {
const folderMetadata = {
name: part,
mimeType: FOLDER_MIME_TYPE,
parents: [currentFolderId]
};
const folder = await drive.files.create({
requestBody: folderMetadata,
fields: "id"
});
if (!folder.data.id) {
throw new Error(`Failed to create intermediate folder: ${part}`);
}
currentFolderId = folder.data.id;
} else {
currentFolderId = response.data.files[0].id;
}
}
return currentFolderId;
}
async function resolveFolderId(input) {
if (!input) return "root";
if (input.startsWith("/")) {
return resolvePath(input);
} else {
return input;
}
}
function validateTextFileExtension(name) {
const ext = getExtensionFromFilename(name);
if (!["txt", "md"].includes(ext)) {
throw new Error("File name must end with .txt or .md for text files.");
}
}
function convertA1ToGridRange(a1Notation, sheetId) {
const rangeRegex = /^([A-Z]*)([0-9]*)(:([A-Z]*)([0-9]*))?$/;
const match = a1Notation.match(rangeRegex);
if (!match) {
throw new Error(`Invalid A1 notation: ${a1Notation}`);
}
const [, startCol, startRow, , endCol, endRow] = match;
const gridRange = { sheetId };
const colToNum = (col) => {
let num = 0;
for (let i = 0; i < col.length; i++) {
num = num * 26 + (col.charCodeAt(i) - "A".charCodeAt(0) + 1);
}
return num - 1;
};
if (startCol) gridRange.startColumnIndex = colToNum(startCol);
if (startRow) gridRange.startRowIndex = parseInt(startRow) - 1;
if (endCol) {
gridRange.endColumnIndex = colToNum(endCol) + 1;
} else if (startCol && !endCol) {
gridRange.endColumnIndex = gridRange.startColumnIndex + 1;
}
if (endRow) {
gridRange.endRowIndex = parseInt(endRow);
} else if (startRow && !endRow) {
gridRange.endRowIndex = gridRange.startRowIndex + 1;
}
return gridRange;
}
async function checkFileExists(name, parentFolderId = "root") {
try {
const escapedName = name.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
const query = `name = '${escapedName}' and '${parentFolderId}' in parents and trashed = false`;
const res = await drive.files.list({
q: query,
fields: "files(id, name, mimeType)",
pageSize: 1
});
if (res.data.files && res.data.files.length > 0) {
return res.data.files[0].id || null;
}
return null;
} catch (error) {
log("Error checking file existence:", error);
return null;
}
}
var SearchSchema = z.object({
query: z.string().min(1, "Search query is required")
});
var CreateTextFileSchema = z.object({
name: z.string().min(1, "File name is required"),
content: z.string(),
parentFolderId: z.string().optional()
});
var UpdateTextFileSchema = z.object({
fileId: z.string().min(1, "File ID is required"),
content: z.string(),
name: z.string().optional()
});
var CreateFolderSchema = z.object({
name: z.string().min(1, "Folder name is required"),
parent: z.string().optional()
});
var ListFolderSchema = z.object({
folderId: z.string().optional(),
pageSize: z.number().min(1).max(100).optional(),
pageToken: z.string().optional()
});
var DeleteItemSchema = z.object({
itemId: z.string().min(1, "Item ID is required")
});
var RenameItemSchema = z.object({
itemId: z.string().min(1, "Item ID is required"),
newName: z.string().min(1, "New name is required")
});
var MoveItemSchema = z.object({
itemId: z.string().min(1, "Item ID is required"),
destinationFolderId: z.string().optional()
});
var CreateGoogleDocSchema = z.object({
name: z.string().min(1, "Document name is required"),
content: z.string(),
parentFolderId: z.string().optional()
});
var UpdateGoogleDocSchema = z.object({
documentId: z.string().min(1, "Document ID is required"),
content: z.string()
});
var CreateGoogleSheetSchema = z.object({
name: z.string().min(1, "Sheet name is required"),
data: z.array(z.array(z.string())),
parentFolderId: z.string().optional()
});
var UpdateGoogleSheetSchema = z.object({
spreadsheetId: z.string().min(1, "Spreadsheet ID is required"),
range: z.string().min(1, "Range is required"),
data: z.array(z.array(z.string()))
});
var GetGoogleSheetContentSchema = z.object({
spreadsheetId: z.string().min(1, "Spreadsheet ID is required"),
range: z.string().min(1, "Range is required")
});
var FormatGoogleSheetCellsSchema = z.object({
spreadsheetId: z.string().min(1, "Spreadsheet ID is required"),
range: z.string().min(1, "Range is required"),
backgroundColor: z.object({
red: z.number().min(0).max(1).optional(),
green: z.number().min(0).max(1).optional(),
blue: z.number().min(0).max(1).optional()
}).optional(),
horizontalAlignment: z.enum(["LEFT", "CENTER", "RIGHT"]).optional(),
verticalAlignment: z.enum(["TOP", "MIDDLE", "BOTTOM"]).optional(),
wrapStrategy: z.enum(["OVERFLOW_CELL", "CLIP", "WRAP"]).optional()
});
var FormatGoogleSheetTextSchema = z.object({
spreadsheetId: z.string().min(1, "Spreadsheet ID is required"),
range: z.string().min(1, "Range is required"),
bold: z.boolean().optional(),
italic: z.boolean().optional(),
strikethrough: z.boolean().optional(),
underline: z.boolean().optional(),
fontSize: z.number().min(1).optional(),
fontFamily: z.string().optional(),
foregroundColor: z.object({
red: z.number().min(0).max(1).optional(),
green: z.number().min(0).max(1).optional(),
blue: z.number().min(0).max(1).optional()
}).optional()
});
var FormatGoogleSheetNumbersSchema = z.object({
spreadsheetId: z.string().min(1, "Spreadsheet ID is required"),
range: z.string().min(1, "Range is required"),
pattern: z.string().min(1, "Pattern is required"),
type: z.enum(["NUMBER", "CURRENCY", "PERCENT", "DATE", "TIME", "DATE_TIME", "SCIENTIFIC"]).optional()
});
var SetGoogleSheetBordersSchema = z.object({
spreadsheetId: z.string().min(1, "Spreadsheet ID is required"),
range: z.string().min(1, "Range is required"),
style: z.enum(["SOLID", "DASHED", "DOTTED", "DOUBLE"]),
width: z.number().min(1).max(3).optional(),
color: z.object({
red: z.number().min(0).max(1).optional(),
green: z.number().min(0).max(1).optional(),
blue: z.number().min(0).max(1).optional()
}).optional(),
top: z.boolean().optional(),
bottom: z.boolean().optional(),
left: z.boolean().optional(),
right: z.boolean().optional(),
innerHorizontal: z.boolean().optional(),
innerVertical: z.boolean().optional()
});
var MergeGoogleSheetCellsSchema = z.object({
spreadsheetId: z.string().min(1, "Spreadsheet ID is required"),
range: z.string().min(1, "Range is required"),
mergeType: z.enum(["MERGE_ALL", "MERGE_COLUMNS", "MERGE_ROWS"])
});
var AddGoogleSheetConditionalFormatSchema = z.object({
spreadsheetId: z.string().min(1, "Spreadsheet ID is required"),
range: z.string().min(1, "Range is required"),
condition: z.object({
type: z.enum(["NUMBER_GREATER", "NUMBER_LESS", "TEXT_CONTAINS", "TEXT_STARTS_WITH", "TEXT_ENDS_WITH", "CUSTOM_FORMULA"]),
value: z.string()
}),
format: z.object({
backgroundColor: z.object({
red: z.number().min(0).max(1).optional(),
green: z.number().min(0).max(1).optional(),
blue: z.number().min(0).max(1).optional()
}).optional(),
textFormat: z.object({
bold: z.boolean().optional(),
foregroundColor: z.object({
red: z.number().min(0).max(1).optional(),
green: z.number().min(0).max(1).optional(),
blue: z.number().min(0).max(1).optional()
}).optional()
}).optional()
})
});
var CreateGoogleSlidesSchema = z.object({
name: z.string().min(1, "Presentation name is required"),
slides: z.array(z.object({
title: z.string(),
content: z.string()
})).min(1, "At least one slide is required"),
parentFolderId: z.string().optional()
});
var UpdateGoogleSlidesSchema = z.object({
presentationId: z.string().min(1, "Presentation ID is required"),
slides: z.array(z.object({
title: z.string(),
content: z.string()
})).min(1, "At least one slide is required")
});
var FormatGoogleDocTextSchema = z.object({
documentId: z.string().min(1, "Document ID is required"),
startIndex: z.number().min(1, "Start index must be at least 1"),
endIndex: z.number().min(1, "End index must be at least 1"),
bold: z.boolean().optional(),
italic: z.boolean().optional(),
underline: z.boolean().optional(),
strikethrough: z.boolean().optional(),
fontSize: z.number().optional(),
foregroundColor: z.object({
red: z.number().min(0).max(1).optional(),
green: z.number().min(0).max(1).optional(),
blue: z.number().min(0).max(1).optional()
}).optional()
});
var FormatGoogleDocParagraphSchema = z.object({
documentId: z.string().min(1, "Document ID is required"),
startIndex: z.number().min(1, "Start index must be at least 1"),
endIndex: z.number().min(1, "End index must be at least 1"),
namedStyleType: z.enum(["NORMAL_TEXT", "TITLE", "SUBTITLE", "HEADING_1", "HEADING_2", "HEADING_3", "HEADING_4", "HEADING_5", "HEADING_6"]).optional(),
alignment: z.enum(["START", "CENTER", "END", "JUSTIFIED"]).optional(),
lineSpacing: z.number().optional(),
spaceAbove: z.number().optional(),
spaceBelow: z.number().optional()
});
var GetGoogleDocContentSchema = z.object({
documentId: z.string().min(1, "Document ID is required")
});
var GetGoogleSlidesContentSchema = z.object({
presentationId: z.string().min(1, "Presentation ID is required"),
slideIndex: z.number().min(0).optional()
});
var FormatGoogleSlidesTextSchema = z.object({
presentationId: z.string().min(1, "Presentation ID is required"),
objectId: z.string().min(1, "Object ID is required"),
startIndex: z.number().min(0).optional(),
endIndex: z.number().min(0).optional(),
bold: z.boolean().optional(),
italic: z.boolean().optional(),
underline: z.boolean().optional(),
strikethrough: z.boolean().optional(),
fontSize: z.number().optional(),
fontFamily: z.string().optional(),
foregroundColor: z.object({
red: z.number().min(0).max(1).optional(),
green: z.number().min(0).max(1).optional(),
blue: z.number().min(0).max(1).optional()
}).optional()
});
var FormatGoogleSlidesParagraphSchema = z.object({
presentationId: z.string().min(1, "Presentation ID is required"),
objectId: z.string().min(1, "Object ID is required"),
alignment: z.enum(["START", "CENTER", "END", "JUSTIFIED"]).optional(),
lineSpacing: z.number().optional(),
bulletStyle: z.enum(["NONE", "DISC", "ARROW", "SQUARE", "DIAMOND", "STAR", "NUMBERED"]).optional()
});
var StyleGoogleSlidesShapeSchema = z.object({
presentationId: z.string().min(1, "Presentation ID is required"),
objectId: z.string().min(1, "Shape object ID is required"),
backgroundColor: z.object({
red: z.number().min(0).max(1).optional(),
green: z.number().min(0).max(1).optional(),
blue: z.number().min(0).max(1).optional(),
alpha: z.number().min(0).max(1).optional()
}).optional(),
outlineColor: z.object({
red: z.number().min(0).max(1).optional(),
green: z.number().min(0).max(1).optional(),
blue: z.number().min(0).max(1).optional()
}).optional(),
outlineWeight: z.number().optional(),
outlineDashStyle: z.enum(["SOLID", "DOT", "DASH", "DASH_DOT", "LONG_DASH", "LONG_DASH_DOT"]).optional()
});
var SetGoogleSlidesBackgroundSchema = z.object({
presentationId: z.string().min(1, "Presentation ID is required"),
pageObjectIds: z.array(z.string()).min(1, "At least one page object ID is required"),
backgroundColor: z.object({
red: z.number().min(0).max(1).optional(),
green: z.number().min(0).max(1).optional(),
blue: z.number().min(0).max(1).optional(),
alpha: z.number().min(0).max(1).optional()
})
});
var CreateGoogleSlidesTextBoxSchema = z.object({
presentationId: z.string().min(1, "Presentation ID is required"),
pageObjectId: z.string().min(1, "Page object ID is required"),
text: z.string().min(1, "Text content is required"),
x: z.number(),
y: z.number(),
width: z.number(),
height: z.number(),
fontSize: z.number().optional(),
bold: z.boolean().optional(),
italic: z.boolean().optional()
});
var CreateGoogleSlidesShapeSchema = z.object({
presentationId: z.string().min(1, "Presentation ID is required"),
pageObjectId: z.string().min(1, "Page object ID is required"),
shapeType: z.enum(["RECTANGLE", "ELLIPSE", "DIAMOND", "TRIANGLE", "STAR", "ROUND_RECTANGLE", "ARROW"]),
x: z.number(),
y: z.number(),
width: z.number(),
height: z.number(),
backgroundColor: z.object({
red: z.number().min(0).max(1).optional(),
green: z.number().min(0).max(1).optional(),
blue: z.number().min(0).max(1).optional(),
alpha: z.number().min(0).max(1).optional()
}).optional()
});
var server = new Server(
{
name: "google-drive-mcp",
version: VERSION
},
{
capabilities: {
resources: {},
tools: {}
}
}
);
async function ensureAuthenticated() {
if (!authClient) {
if (authenticationPromise) {
log("Authentication already in progress, waiting...");
authClient = await authenticationPromise;
return;
}
log("Initializing authentication");
authenticationPromise = authenticate();
try {
authClient = await authenticationPromise;
log("Authentication complete", {
authClientType: authClient?.constructor?.name,
hasCredentials: !!authClient?.credentials,
hasAccessToken: !!authClient?.credentials?.access_token
});
ensureDriveService();
} finally {
authenticationPromise = null;
}
}
ensureDriveService();
}
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
await ensureAuthenticated();
log("Handling ListResources request", { params: request.params });
const pageSize = 10;
const params = {
pageSize,
fields: "nextPageToken, files(id, name, mimeType)",
q: `trashed = false`
};
if (request.params?.cursor) {
params.pageToken = request.params.cursor;
}
const res = await drive.files.list(params);
log("Listed files", { count: res.data.files?.length });
const files = res.data.files || [];
return {
resources: files.map((file) => ({
uri: `gdrive:///${file.id}`,
mimeType: file.mimeType || "application/octet-stream",
name: file.name || "Untitled"
})),
nextCursor: res.data.nextPageToken
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
await ensureAuthenticated();
log("Handling ReadResource request", { uri: request.params.uri });
const fileId = request.params.uri.replace("gdrive:///", "");
const file = await drive.files.get({
fileId,
fields: "mimeType"
});
const mimeType = file.data.mimeType;
if (!mimeType) {
throw new Error("File has no MIME type.");
}
if (mimeType.startsWith("application/vnd.google-apps")) {
let exportMimeType;
switch (mimeType) {
case "application/vnd.google-apps.document":
exportMimeType = "text/markdown";
break;
case "application/vnd.google-apps.spreadsheet":
exportMimeType = "text/csv";
break;
case "application/vnd.google-apps.presentation":
exportMimeType = "text/plain";
break;
case "application/vnd.google-apps.drawing":
exportMimeType = "image/png";
break;
default:
exportMimeType = "text/plain";
break;
}
const res = await drive.files.export(
{ fileId, mimeType: exportMimeType },
{ responseType: "text" }
);
log("Successfully read resource", { fileId, mimeType });
return {
contents: [
{
uri: request.params.uri,
mimeType: exportMimeType,
text: res.data
}
]
};
} else {
const res = await drive.files.get(
{ fileId, alt: "media" },
{ responseType: "arraybuffer" }
);
const contentMime = mimeType || "application/octet-stream";
if (contentMime.startsWith("text/") || contentMime === "application/json") {
return {
contents: [
{
uri: request.params.uri,
mimeType: contentMime,
text: Buffer.from(res.data).toString("utf-8")
}
]
};
} else {
return {
contents: [
{
uri: request.params.uri,
mimeType: contentMime,
blob: Buffer.from(res.data).toString("base64")
}
]
};
}
}
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "search",
description: "Search for files in Google Drive",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query" }
},
required: ["query"]
}
},
{
name: "createTextFile",
description: "Create a new text or markdown file",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "File name (.txt or .md)" },
content: { type: "string", description: "File content" },
parentFolderId: { type: "string", description: "Optional parent folder ID", optional: true }
},
required: ["name", "content"]
}
},
{
name: "updateTextFile",
description: "Update an existing text or markdown file",
inputSchema: {
type: "object",
properties: {
fileId: { type: "string", description: "ID of the file to update" },
content: { type: "string", description: "New file content" },
name: { type: "string", description: "Optional new name (.txt or .md)", optional: true }
},
required: ["fileId", "content"]
}
},
{
name: "createFolder",
description: "Create a new folder in Google Drive",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Folder name" },
parent: { type: "string", description: "Optional parent folder ID or path", optional: true }
},
required: ["name"]
}
},
{
name: "listFolder",
description: "List contents of a folder (defaults to root)",
inputSchema: {
type: "object",
properties: {
folderId: { type: "string", description: "Folder ID", optional: true },
pageSize: { type: "number", description: "Items to return (default 50, max 100)", optional: true },
pageToken: { type: "string", description: "Token for next page", optional: true }
}
}
},
{
name: "deleteItem",
description: "Move a file or folder to trash (can be restored from Google Drive trash)",
inputSchema: {
type: "object",
properties: {
itemId: { type: "string", description: "ID of the item to delete" }
},
required: ["itemId"]
}
},
{
name: "renameItem",
description: "Rename a file or folder",
inputSchema: {
type: "object",
properties: {
itemId: { type: "string", description: "ID of the item to rename" },
newName: { type: "string", description: "New name" }
},
required: ["itemId", "newName"]
}
},
{
name: "moveItem",
description: "Move a file or folder",
inputSchema: {
type: "object",
properties: {
itemId: { type: "string", description: "ID of the item to move" },
destinationFolderId: { type: "string", description: "Destination folder ID", optional: true }
},
required: ["itemId"]
}
},
{
name: "createGoogleDoc",
description: "Create a new Google Doc",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Doc name" },
content: { type: "string", description: "Doc content" },
parentFolderId: { type: "string", description: "Parent folder ID", optional: true }
},
required: ["name", "content"]
}
},
{
name: "updateGoogleDoc",
description: "Update an existing Google Doc",
inputSchema: {
type: "object",
properties: {
documentId: { type: "string", description: "Doc ID" },
content: { type: "string", description: "New content" }
},
required: ["documentId", "content"]
}
},
{
name: "createGoogleSheet",
description: "Create a new Google Sheet",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Sheet name" },
data: {
type: "array",
description: "Data as array of arrays",
items: { type: "array", items: { type: "string" } }
},
parentFolderId: { type: "string", description: "Parent folder ID (defaults to root)", optional: true }
},
required: ["name", "data"]
}
},
{
name: "updateGoogleSheet",
description: "Update an existing Google Sheet",
inputSchema: {
type: "object",
properties: {
spreadsheetId: { type: "string", description: "Sheet ID" },
range: { type: "string", description: "Range to update" },
data: {
type: "array",
items: { type: "array", items: { type: "string" } }
}
},
required: ["spreadsheetId", "range", "data"]
}
},
{
name: "getGoogleSheetContent",
description: "Get content of a Google Sheet with cell information",
inputSchema: {
type: "object",
properties: {
spreadsheetId: { type: "string", description: "Spreadsheet ID" },
range: { type: "string", description: "Range to get (e.g., 'Sheet1!A1:C10')" }
},
required: ["spreadsheetId", "range"]
}
},
{
name: "formatGoogleSheetCells",
description: "Format cells in a Google Sheet (background, borders, alignment)",
inputSchema: {
type: "object",
properties: {
spreadsheetId: { type: "string", description: "Spreadsheet ID" },
range: { type: "string", description: "Range to format (e.g., 'A1:C10')" },
backgroundColor: {
type: "object",
description: "Background color (RGB values 0-1)",
properties: {
red: { type: "number", optional: true },
green: { type: "number", optional: true },
blue: { type: "number", optional: true }
},
optional: true
},
horizontalAlignment: {
type: "string",
description: "Horizontal alignment",
enum: ["LEFT", "CENTER", "RIGHT"],
optional: true
},
verticalAlignment: {
type: "string",
description: "Vertical alignment",
enum: ["TOP", "MIDDLE", "BOTTOM"],
optional: true
},
wrapStrategy: {
type: "string",
description: "Text wrapping",
enum: ["OVERFLOW_CELL", "CLIP", "WRAP"],
optional: true
}
},
required: ["spreadsheetId", "range"]
}
},
{
name: "formatGoogleSheetText",
description: "Apply text formatting to cells in a Google Sheet",
inputSchema: {
type: "object",
properties: {
spreadsheetId: { type: "string", description: "Spreadsheet ID" },
range: { type: "string", description: "Range to format (e.g., 'A1:C10')" },
bold: { type: "boolean", description: "Make text bold", optional: true },
italic: { type: "boolean", description: "Make text italic", optional: true },
strikethrough: { type: "boolean", description: "Strikethrough text", optional: true },
underline: { type: "boolean", description: "Underline text", optional: true },
fontSize: { type: "number", description: "Font size in points", optional: true },
fontFamily: { type: "string", description: "Font family name", optional: true },
foregroundColor: {
type: "object",
description: "Text color (RGB values 0-1)",
properties: {
red: { type: "number", optional: true },
green: { type: "number", optional: true },
blue: { type: "number", optional: true }
},
optional: true
}
},
required: ["spreadsheetId", "range"]
}
},
{
name: "formatGoogleSheetNumbers",
description: "Apply number formatting to cells in a Google Sheet",
inputSchema: {
type: "object",
properties: {
spreadsheetId: { type: "string", description: "Spreadsheet ID" },
range: { type: "string", description: "Range to format (e.g., 'A1:C10')" },
pattern: {
type: "string",
description: "Number format pattern (e.g., '#,##0.00', 'yyyy-mm-dd', '$#,##0.00', '0.00%')"
},
type: {
type: "string",
description: "Format type",
enum: ["NUMBER", "CURRENCY", "PERCENT