mcp-server-collector
by chatmcp
#!/usr/bin/env node
import { authenticate } from "@google-cloud/local-auth";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
ErrorCode,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
import fs from "fs";
import { google } from "googleapis";
import path from "path";
const drive = google.drive("v3");
const server = new Server(
{
name: "gdrive",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {},
},
},
);
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
const pageSize = 10;
const params: any = {
pageSize,
fields: "nextPageToken, files(id, name, mimeType)",
};
if (request.params?.cursor) {
params.pageToken = request.params.cursor;
}
const res = await drive.files.list(params);
const files = res.data.files!;
return {
resources: files.map((file) => ({
uri: `gdrive:///${file.id}`,
mimeType: file.mimeType,
name: file.name,
})),
nextCursor: res.data.nextPageToken,
};
});
async function readFileContent(fileId: string) {
// First get file metadata to check mime type
const file = await drive.files.get({
fileId,
fields: "mimeType",
});
// For Google Docs/Sheets/etc we need to export
if (file.data.mimeType?.startsWith("application/vnd.google-apps")) {
let exportMimeType: string;
switch (file.data.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";
}
const res = await drive.files.export(
{ fileId, mimeType: exportMimeType },
{ responseType: "text" },
);
return {
mimeType: exportMimeType,
content: res.data,
};
}
// For regular files download content
const res = await drive.files.get(
{ fileId, alt: "media" },
{ responseType: "arraybuffer" },
);
const mimeType = file.data.mimeType || "application/octet-stream";
if (mimeType.startsWith("text/") || mimeType === "application/json") {
return {
mimeType: mimeType,
content: Buffer.from(res.data as ArrayBuffer).toString("utf-8"),
};
} else {
return {
mimeType: mimeType,
content: Buffer.from(res.data as ArrayBuffer).toString("base64"),
};
}
}
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const fileId = request.params.uri.replace("gdrive:///", "");
const result = await readFileContent(fileId);
return {
contents: [
{
uri: request.params.uri,
mimeType: result.mimeType,
text: result.content,
},
],
};
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "gdrive_search",
description: "Search for files specifically in your Google Drive account (don't use exa nor brave to search for files)",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query",
},
},
required: ["query"],
},
},
{
name: "gdrive_read_file",
description: "Read a file from Google Drive using its Google Drive file ID (don't use exa nor brave to read files)",
inputSchema: {
type: "object",
properties: {
file_id: {
type: "string",
description: "The ID of the file to read",
},
},
required: ["file_id"],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "gdrive_search") {
const userQuery = request.params.arguments?.query as string;
const escapedQuery = userQuery.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
const formattedQuery = `fullText contains '${escapedQuery}'`;
const res = await drive.files.list({
q: formattedQuery,
pageSize: 10,
fields: "files(id, name, mimeType, modifiedTime, size)",
});
const fileList = res.data.files
?.map((file: any) => `${file.name} (${file.mimeType}) - ID: ${file.id}`)
.join("\n");
return {
content: [
{
type: "text",
text: `Found ${res.data.files?.length ?? 0} files:\n${fileList}`,
},
],
isError: false,
};
} else if (request.params.name === "gdrive_read_file") {
const fileId = request.params.arguments?.file_id as string;
if (!fileId) {
throw new McpError(ErrorCode.InvalidParams, "File ID is required");
}
try {
const result = await readFileContent(fileId);
return {
content: [
{
type: "text",
text: result.content,
},
],
isError: false,
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: `Error reading file: ${error.message}`,
},
],
isError: true,
};
}
}
throw new Error("Tool not found");
});
const credentialsPath = process.env.MCP_GDRIVE_CREDENTIALS || path.join(process.cwd(), "credentials", ".gdrive-server-credentials.json");
async function authenticateAndSaveCredentials() {
const keyPath = process.env.GOOGLE_APPLICATION_CREDENTIALS || path.join(process.cwd(), "credentials", "gcp-oauth.keys.json");
console.log("Looking for keys at:", keyPath);
console.log("Will save credentials to:", credentialsPath);
const auth = await authenticate({
keyfilePath: keyPath,
scopes: ["https://www.googleapis.com/auth/drive.readonly"],
});
fs.writeFileSync(credentialsPath, JSON.stringify(auth.credentials));
console.log("Credentials saved. You can now run the server.");
}
async function loadCredentialsAndRunServer() {
if (!fs.existsSync(credentialsPath)) {
console.error(
"Credentials not found. Please run with 'auth' argument first.",
);
process.exit(1);
}
const credentials = JSON.parse(fs.readFileSync(credentialsPath, "utf-8"));
const auth = new google.auth.OAuth2();
auth.setCredentials(credentials);
google.options({ auth });
const transport = new StdioServerTransport();
await server.connect(transport);
}
if (process.argv[2] === "auth") {
authenticateAndSaveCredentials().catch(console.error);
} else {
loadCredentialsAndRunServer().catch((error) => {
process.stderr.write(`Error: ${error}\n`);
process.exit(1);
});
}