Google Workspace MCP Server
by epaproditus
#!/usr/bin/env node
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';
// Environment variables required for OAuth
const CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
const REFRESH_TOKEN = process.env.GOOGLE_REFRESH_TOKEN;
if (!CLIENT_ID || !CLIENT_SECRET || !REFRESH_TOKEN) {
throw new Error('Required Google OAuth credentials not found in environment variables');
}
class GoogleWorkspaceServer {
private server: Server;
private auth;
private gmail;
private calendar;
constructor() {
this.server = new Server(
{
name: 'google-workspace-server',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
// Set up OAuth2 client
this.auth = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET);
this.auth.setCredentials({ refresh_token: REFRESH_TOKEN });
// Initialize API clients
this.gmail = google.gmail({ version: 'v1', auth: this.auth });
this.calendar = google.calendar({ version: 'v3', auth: this.auth });
this.setupToolHandlers();
// Error handling
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'list_emails',
description: 'List recent emails from Gmail inbox',
inputSchema: {
type: 'object',
properties: {
maxResults: {
type: 'number',
description: 'Maximum number of emails to return (default: 10)',
},
query: {
type: 'string',
description: 'Search query to filter emails',
},
},
},
},
{
name: 'search_emails',
description: 'Search emails with advanced query',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Gmail search query (e.g., "from:example@gmail.com has:attachment")',
required: true
},
maxResults: {
type: 'number',
description: 'Maximum number of emails to return (default: 10)',
},
},
required: ['query']
},
},
{
name: 'send_email',
description: 'Send a new email',
inputSchema: {
type: 'object',
properties: {
to: {
type: 'string',
description: 'Recipient email address',
},
subject: {
type: 'string',
description: 'Email subject',
},
body: {
type: 'string',
description: 'Email body (can include HTML)',
},
cc: {
type: 'string',
description: 'CC recipients (comma-separated)',
},
bcc: {
type: 'string',
description: 'BCC recipients (comma-separated)',
},
},
required: ['to', 'subject', 'body']
},
},
{
name: 'modify_email',
description: 'Modify email labels (archive, trash, mark read/unread)',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Email ID',
},
addLabels: {
type: 'array',
items: { type: 'string' },
description: 'Labels to add',
},
removeLabels: {
type: 'array',
items: { type: 'string' },
description: 'Labels to remove',
},
},
required: ['id']
},
},
{
name: 'list_events',
description: 'List upcoming calendar events',
inputSchema: {
type: 'object',
properties: {
maxResults: {
type: 'number',
description: 'Maximum number of events to return (default: 10)',
},
timeMin: {
type: 'string',
description: 'Start time in ISO format (default: now)',
},
timeMax: {
type: 'string',
description: 'End time in ISO format',
},
},
},
},
{
name: 'create_event',
description: 'Create a new calendar event',
inputSchema: {
type: 'object',
properties: {
summary: {
type: 'string',
description: 'Event title',
},
location: {
type: 'string',
description: 'Event location',
},
description: {
type: 'string',
description: 'Event description',
},
start: {
type: 'string',
description: 'Start time in ISO format',
},
end: {
type: 'string',
description: 'End time in ISO format',
},
attendees: {
type: 'array',
items: { type: 'string' },
description: 'List of attendee email addresses',
},
},
required: ['summary', 'start', 'end']
},
},
{
name: 'update_event',
description: 'Update an existing calendar event',
inputSchema: {
type: 'object',
properties: {
eventId: {
type: 'string',
description: 'Event ID to update',
},
summary: {
type: 'string',
description: 'New event title',
},
location: {
type: 'string',
description: 'New event location',
},
description: {
type: 'string',
description: 'New event description',
},
start: {
type: 'string',
description: 'New start time in ISO format',
},
end: {
type: 'string',
description: 'New end time in ISO format',
},
attendees: {
type: 'array',
items: { type: 'string' },
description: 'New list of attendee email addresses',
},
},
required: ['eventId']
},
},
{
name: 'delete_event',
description: 'Delete a calendar event',
inputSchema: {
type: 'object',
properties: {
eventId: {
type: 'string',
description: 'Event ID to delete',
},
},
required: ['eventId']
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case 'list_emails':
return await this.handleListEmails(request.params.arguments);
case 'search_emails':
return await this.handleSearchEmails(request.params.arguments);
case 'send_email':
return await this.handleSendEmail(request.params.arguments);
case 'modify_email':
return await this.handleModifyEmail(request.params.arguments);
case 'list_events':
return await this.handleListEvents(request.params.arguments);
case 'create_event':
return await this.handleCreateEvent(request.params.arguments);
case 'update_event':
return await this.handleUpdateEvent(request.params.arguments);
case 'delete_event':
return await this.handleDeleteEvent(request.params.arguments);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
});
}
private async handleListEmails(args: any) {
try {
const maxResults = args?.maxResults || 10;
const query = args?.query || '';
const response = await this.gmail.users.messages.list({
userId: 'me',
maxResults,
q: query,
});
const messages = response.data.messages || [];
const emailDetails = await Promise.all(
messages.map(async (msg) => {
const detail = await this.gmail.users.messages.get({
userId: 'me',
id: msg.id!,
});
const headers = detail.data.payload?.headers;
const subject = headers?.find((h) => h.name === 'Subject')?.value || '';
const from = headers?.find((h) => h.name === 'From')?.value || '';
const date = headers?.find((h) => h.name === 'Date')?.value || '';
return {
id: msg.id,
subject,
from,
date,
};
})
);
return {
content: [
{
type: 'text',
text: JSON.stringify(emailDetails, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error fetching emails: ${error.message}`,
},
],
isError: true,
};
}
}
private async handleSearchEmails(args: any) {
try {
const maxResults = args?.maxResults || 10;
const query = args?.query || '';
const response = await this.gmail.users.messages.list({
userId: 'me',
maxResults,
q: query,
});
const messages = response.data.messages || [];
const emailDetails = await Promise.all(
messages.map(async (msg) => {
const detail = await this.gmail.users.messages.get({
userId: 'me',
id: msg.id!,
});
const headers = detail.data.payload?.headers;
const subject = headers?.find((h) => h.name === 'Subject')?.value || '';
const from = headers?.find((h) => h.name === 'From')?.value || '';
const date = headers?.find((h) => h.name === 'Date')?.value || '';
return {
id: msg.id,
subject,
from,
date,
};
})
);
return {
content: [
{
type: 'text',
text: JSON.stringify(emailDetails, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error fetching emails: ${error.message}`,
},
],
isError: true,
};
}
}
private async handleSendEmail(args: any) {
try {
const { to, subject, body, cc, bcc } = args;
// Create email content
const message = [
'Content-Type: text/html; charset=utf-8',
'MIME-Version: 1.0',
`To: ${to}`,
cc ? `Cc: ${cc}` : '',
bcc ? `Bcc: ${bcc}` : '',
`Subject: ${subject}`,
'',
body,
].filter(Boolean).join('\r\n');
// Encode the email
const encodedMessage = Buffer.from(message)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
// Send the email
const response = await this.gmail.users.messages.send({
userId: 'me',
requestBody: {
raw: encodedMessage,
},
});
return {
content: [
{
type: 'text',
text: `Email sent successfully. Message ID: ${response.data.id}`,
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error sending email: ${error.message}`,
},
],
isError: true,
};
}
}
private async handleModifyEmail(args: any) {
try {
const { id, addLabels = [], removeLabels = [] } = args;
const response = await this.gmail.users.messages.modify({
userId: 'me',
id,
requestBody: {
addLabelIds: addLabels,
removeLabelIds: removeLabels,
},
});
return {
content: [
{
type: 'text',
text: `Email modified successfully. Updated labels for message ID: ${response.data.id}`,
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error modifying email: ${error.message}`,
},
],
isError: true,
};
}
}
private async handleCreateEvent(args: any) {
try {
const { summary, location, description, start, end, attendees = [] } = args;
const event = {
summary,
location,
description,
start: {
dateTime: start,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
end: {
dateTime: end,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
attendees: attendees.map((email: string) => ({ email })),
};
const response = await this.calendar.events.insert({
calendarId: 'primary',
requestBody: event,
});
return {
content: [
{
type: 'text',
text: `Event created successfully. Event ID: ${response.data.id}`,
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error creating event: ${error.message}`,
},
],
isError: true,
};
}
}
private async handleUpdateEvent(args: any) {
try {
const { eventId, summary, location, description, start, end, attendees } = args;
const event: any = {};
if (summary) event.summary = summary;
if (location) event.location = location;
if (description) event.description = description;
if (start) {
event.start = {
dateTime: start,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
};
}
if (end) {
event.end = {
dateTime: end,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
};
}
if (attendees) {
event.attendees = attendees.map((email: string) => ({ email }));
}
const response = await this.calendar.events.patch({
calendarId: 'primary',
eventId,
requestBody: event,
});
return {
content: [
{
type: 'text',
text: `Event updated successfully. Event ID: ${response.data.id}`,
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error updating event: ${error.message}`,
},
],
isError: true,
};
}
}
private async handleDeleteEvent(args: any) {
try {
const { eventId } = args;
await this.calendar.events.delete({
calendarId: 'primary',
eventId,
});
return {
content: [
{
type: 'text',
text: `Event deleted successfully. Event ID: ${eventId}`,
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error deleting event: ${error.message}`,
},
],
isError: true,
};
}
}
private async handleListEvents(args: any) {
try {
const maxResults = args?.maxResults || 10;
const timeMin = args?.timeMin || new Date().toISOString();
const timeMax = args?.timeMax;
const response = await this.calendar.events.list({
calendarId: 'primary',
timeMin,
timeMax,
maxResults,
singleEvents: true,
orderBy: 'startTime',
});
const events = response.data.items?.map((event) => ({
id: event.id,
summary: event.summary,
start: event.start,
end: event.end,
location: event.location,
}));
return {
content: [
{
type: 'text',
text: JSON.stringify(events, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error fetching calendar events: ${error.message}`,
},
],
isError: true,
};
}
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Google Workspace MCP server running on stdio');
}
}
const server = new GoogleWorkspaceServer();
server.run().catch(console.error);