agenda-mcp-google-workspace
Version:
MCP Server para integração com Google Workspace (Gmail, Calendar, Meet)
1,009 lines (1,008 loc) • 38.7 kB
JavaScript
#!/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";
import { getCredentials } from "./config.js";
// Obter credenciais do arquivo .env ou argumentos da linha de comando
const { clientId, clientSecret, refreshToken } = getCredentials();
// Atualizar a lista de escopos necessários no construtor ou onde a autenticação é configurada
const REQUIRED_SCOPES = [
'https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/calendar.events',
'https://www.googleapis.com/auth/meetings.space.created'
];
class GoogleWorkspaceServer {
server;
auth;
gmail;
calendar;
constructor() {
this.server = new Server({
name: "agenda-mcp-google-workspace",
version: "1.0.0",
}, {
capabilities: {
tools: {},
}
});
// Set up OAuth2 client with all required scopes
this.auth = new google.auth.OAuth2(clientId, clientSecret);
this.auth.setCredentials({
refresh_token: refreshToken,
scope: REQUIRED_SCOPES.join(' ')
});
// 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);
});
}
async run() {
try {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Google Workspace MCP server running on stdio");
}
catch (error) {
console.error("[Server Error]", error);
process.exit(1);
}
}
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"). Examples:\n' +
'- "from:alice@example.com" (Emails from Alice)\n' +
'- "to:bob@example.com" (Emails sent to Bob)\n' +
'- "subject:Meeting Update" (Emails with "Meeting Update" in the subject)\n' +
'- "has:attachment filename:pdf" (Emails with PDF attachments)\n' +
'- "after:2024/01/01 before:2024/02/01" (Emails between specific dates)\n' +
'- "is:unread" (Unread emails)\n' +
'- "from:@company.com has:attachment" (Emails from a company domain with attachments)',
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"],
},
},
{
name: "list_meetings",
description: "List upcoming Google Meet meetings",
inputSchema: {
type: "object",
properties: {
max_results: {
type: "number",
description: "Maximum number of results to return (default: 10)"
},
time_min: {
type: "string",
description: "Start time in ISO format (default: now)"
},
time_max: {
type: "string",
description: "End time in ISO format (optional)"
}
}
}
},
{
name: "get_meeting",
description: "Get details of a specific Google Meet meeting",
inputSchema: {
type: "object",
properties: {
meeting_id: {
type: "string",
description: "ID of the meeting to retrieve"
}
},
required: ["meeting_id"]
}
},
{
name: "create_meeting",
description: "Create a new Google Meet meeting",
inputSchema: {
type: "object",
properties: {
summary: {
type: "string",
description: "Title of the meeting"
},
description: {
type: "string",
description: "Description for the meeting (optional)"
},
start_time: {
type: "string",
description: "Start time in ISO format"
},
end_time: {
type: "string",
description: "End time in ISO format"
},
attendees: {
type: "array",
description: "List of email addresses for attendees (optional)",
items: {
type: "string"
}
}
},
required: ["summary", "start_time", "end_time"]
}
},
{
name: "update_meeting",
description: "Update an existing Google Meet meeting",
inputSchema: {
type: "object",
properties: {
meeting_id: {
type: "string",
description: "ID of the meeting to update"
},
summary: {
type: "string",
description: "Updated title of the meeting (optional)"
},
description: {
type: "string",
description: "Updated description for the meeting (optional)"
},
start_time: {
type: "string",
description: "Updated start time in ISO format (optional)"
},
end_time: {
type: "string",
description: "Updated end time in ISO format (optional)"
},
attendees: {
type: "array",
description: "Updated list of email addresses for attendees (optional)",
items: {
type: "string"
}
}
},
required: ["meeting_id"]
}
},
{
name: "delete_meeting",
description: "Delete a Google Meet meeting",
inputSchema: {
type: "object",
properties: {
meeting_id: {
type: "string",
description: "ID of the meeting to delete"
}
},
required: ["meeting_id"]
}
}
],
}));
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);
case "list_meetings":
return await this.handleListMeetings(request.params.arguments);
case "get_meeting":
return await this.handleGetMeeting(request.params.arguments);
case "create_meeting":
return await this.handleCreateMeeting(request.params.arguments);
case "update_meeting":
return await this.handleUpdateMeeting(request.params.arguments);
case "delete_meeting":
return await this.handleDeleteMeeting(request.params.arguments);
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
}
});
}
async handleListEmails(args) {
try {
const maxResults = args?.maxResults || 10;
const query = args?.query || "";
const getEmailBody = (payload) => {
if (!payload)
return "";
if (payload.body && payload.body.data) {
return Buffer.from(payload.body.data, "base64").toString("utf-8");
}
if (payload.parts && payload.parts.length > 0) {
for (const part of payload.parts) {
if (part.mimeType === "text/plain") {
return Buffer.from(part.body.data, "base64").toString("utf-8");
}
}
}
return "(No body content)";
};
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 || "";
const body = getEmailBody(detail.data.payload);
return {
id: msg.id,
subject,
from,
date,
body,
};
}));
return {
content: [
{
type: "text",
text: JSON.stringify(emailDetails, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching emails: ${error.message}`,
},
],
isError: true,
};
}
}
async handleSearchEmails(args) {
try {
const maxResults = args?.maxResults || 10;
const query = args?.query || "";
const getEmailBody = (payload) => {
if (!payload)
return "";
if (payload.body && payload.body.data) {
return Buffer.from(payload.body.data, "base64").toString("utf-8");
}
if (payload.parts && payload.parts.length > 0) {
for (const part of payload.parts) {
if (part.mimeType === "text/plain") {
return Buffer.from(part.body.data, "base64").toString("utf-8");
}
}
}
return "(No body content)";
};
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 || "";
const body = getEmailBody(detail.data.payload);
// Use helper function to extract the email body correctly
return {
id: msg.id,
subject,
from,
date,
body,
};
}));
return {
content: [
{
type: "text",
text: JSON.stringify(emailDetails, null, 2),
},
],
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching emails: ${error.message}`,
},
],
isError: true,
};
}
}
async handleSendEmail(args) {
try {
const { to, subject, body, cc, bcc } = args;
// Função auxiliar para codificar o assunto em UTF-8 e base64
const encodeSubject = (text) => {
return `=?UTF-8?B?${Buffer.from(text).toString('base64')}?=`;
};
const headers = [
'Content-Type: text/plain; charset="UTF-8"',
"MIME-Version: 1.0",
`To: ${to}`,
cc ? `Cc: ${cc}` : null,
bcc ? `Bcc: ${bcc}` : null,
`Subject: ${encodeSubject(subject)}`,
]
.filter(Boolean)
.join("\r\n");
// Ensure proper separation between headers and body
const email = `${headers}\r\n\r\n${body}`;
// Encode in base64url
const encodedMessage = Buffer.from(email)
.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) {
return {
content: [
{
type: "text",
text: `Error sending email: ${error.message}`,
},
],
isError: true,
};
}
}
async handleModifyEmail(args) {
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) {
return {
content: [
{
type: "text",
text: `Error modifying email: ${error.message}`,
},
],
isError: true,
};
}
}
async handleCreateEvent(args) {
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) => ({ 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) {
return {
content: [
{
type: "text",
text: `Error creating event: ${error.message}`,
},
],
isError: true,
};
}
}
async handleUpdateEvent(args) {
try {
const { eventId, summary, location, description, start, end, attendees } = args;
const event = {};
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) => ({ 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) {
return {
content: [
{
type: "text",
text: `Error updating event: ${error.message}`,
},
],
isError: true,
};
}
}
async handleDeleteEvent(args) {
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) {
return {
content: [
{
type: "text",
text: `Error deleting event: ${error.message}`,
},
],
isError: true,
};
}
}
async handleListEvents(args) {
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) {
return {
content: [
{
type: "text",
text: `Error fetching calendar events: ${error.message}`,
},
],
isError: true,
};
}
}
async handleListMeetings(args) {
try {
const maxResults = args?.max_results || 10;
const timeMin = args?.time_min || new Date().toISOString();
const timeMax = args?.time_max;
const response = await this.calendar.events.list({
calendarId: "primary",
timeMin,
timeMax,
maxResults,
singleEvents: true,
orderBy: "startTime",
q: "meets.google.com" // Filtra apenas eventos com link do Meet
});
const meetings = response.data.items?.map(event => ({
id: event.id,
summary: event.summary,
description: event.description,
start: event.start,
end: event.end,
meetLink: event.hangoutLink,
attendees: event.attendees?.map(a => a.email) || []
}));
return {
content: [
{
type: "text",
text: JSON.stringify(meetings, null, 2)
}
]
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error listing meetings: ${error.message}`
}
],
isError: true
};
}
}
async handleGetMeeting(args) {
try {
const { meeting_id } = args;
const response = await this.calendar.events.get({
calendarId: "primary",
eventId: meeting_id
});
const meeting = {
id: response.data.id,
summary: response.data.summary,
description: response.data.description,
start: response.data.start,
end: response.data.end,
meetLink: response.data.hangoutLink,
attendees: response.data.attendees?.map(a => a.email) || []
};
return {
content: [
{
type: "text",
text: JSON.stringify(meeting, null, 2)
}
]
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error getting meeting: ${error.message}`
}
],
isError: true
};
}
}
async handleCreateMeeting(args) {
try {
const { summary, description, start_time, end_time, attendees = [] } = args;
const event = {
summary,
description,
start: {
dateTime: start_time,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
},
end: {
dateTime: end_time,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
},
attendees: attendees.map((email) => ({ email })),
conferenceData: {
createRequest: {
requestId: Math.random().toString(36).substring(2),
conferenceSolutionKey: { type: "hangoutsMeet" }
}
}
};
const response = await this.calendar.events.insert({
calendarId: "primary",
requestBody: event,
conferenceDataVersion: 1
});
return {
content: [
{
type: "text",
text: `Meeting created successfully. Meeting ID: ${response.data.id}\nMeet Link: ${response.data.hangoutLink}`
}
]
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error creating meeting: ${error.message}`
}
],
isError: true
};
}
}
async handleUpdateMeeting(args) {
try {
const { meeting_id, summary, description, start_time, end_time, attendees } = args;
const event = {};
if (summary)
event.summary = summary;
if (description)
event.description = description;
if (start_time) {
event.start = {
dateTime: start_time,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
};
}
if (end_time) {
event.end = {
dateTime: end_time,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
};
}
if (attendees) {
event.attendees = attendees.map((email) => ({ email }));
}
const response = await this.calendar.events.patch({
calendarId: "primary",
eventId: meeting_id,
requestBody: event
});
return {
content: [
{
type: "text",
text: `Meeting updated successfully. Meeting ID: ${response.data.id}`
}
]
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error updating meeting: ${error.message}`
}
],
isError: true
};
}
}
async handleDeleteMeeting(args) {
try {
const { meeting_id } = args;
await this.calendar.events.delete({
calendarId: "primary",
eventId: meeting_id
});
return {
content: [
{
type: "text",
text: `Meeting deleted successfully. Meeting ID: ${meeting_id}`
}
]
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error deleting meeting: ${error.message}`
}
],
isError: true
};
}
}
}
// Inicializar e iniciar o servidor
const server = new GoogleWorkspaceServer();
server.run().catch((error) => {
console.error("[Fatal Error]", error);
process.exit(1);
});