index.ts•15.6 kB
#!/usr/bin/env node
/**
* Google Calendar MCP Server
*
* This MCP server provides tools for integrating with Google Calendar API.
* It handles authentication, event management, and calendar operations
* through the Model Context Protocol.
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { GoogleAuth } from "google-auth-library";
import { google } from "googleapis";
import * as dotenv from "dotenv";
// Load environment variables
dotenv.config();
// Google Calendar API setup
const auth = new GoogleAuth({
keyFile: process.env.GOOGLE_SERVICE_ACCOUNT_KEY_FILE,
scopes: [
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/calendar.events",
],
});
const calendar = google.calendar({ version: "v3", auth });
// Tool schemas
const ListEventsArgsSchema = z.object({
calendarId: z.string().optional().default("primary"),
timeMin: z.string().optional(),
timeMax: z.string().optional(),
maxResults: z.number().optional().default(250),
singleEvents: z.boolean().optional().default(true),
orderBy: z.enum(["startTime", "updated"]).optional().default("startTime"),
});
const CreateEventArgsSchema = z.object({
calendarId: z.string().optional().default("primary"),
summary: z.string(),
description: z.string().optional(),
start: z.object({
dateTime: z.string(),
timeZone: z.string().optional(),
}),
end: z.object({
dateTime: z.string(),
timeZone: z.string().optional(),
}),
location: z.string().optional(),
attendees: z.array(z.object({
email: z.string(),
displayName: z.string().optional(),
})).optional(),
recurrence: z.array(z.string()).optional(),
});
const UpdateEventArgsSchema = z.object({
calendarId: z.string().optional().default("primary"),
eventId: z.string(),
summary: z.string().optional(),
description: z.string().optional(),
start: z.object({
dateTime: z.string(),
timeZone: z.string().optional(),
}).optional(),
end: z.object({
dateTime: z.string(),
timeZone: z.string().optional(),
}).optional(),
location: z.string().optional(),
attendees: z.array(z.object({
email: z.string(),
displayName: z.string().optional(),
})).optional(),
});
const DeleteEventArgsSchema = z.object({
calendarId: z.string().optional().default("primary"),
eventId: z.string(),
});
const ListCalendarsArgsSchema = z.object({
minAccessRole: z.enum(["freeBusyReader", "owner", "reader", "writer"]).optional(),
showDeleted: z.boolean().optional().default(false),
showHidden: z.boolean().optional().default(false),
});
// MCP Server setup
const server = new Server(
{
name: "google-calendar-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Tool definitions
const tools: Tool[] = [
{
name: "list_events",
description: "List events from a Google Calendar",
inputSchema: {
type: "object",
properties: {
calendarId: {
type: "string",
description: "Calendar ID (default: 'primary')",
default: "primary",
},
timeMin: {
type: "string",
description: "Lower bound for event start time (RFC3339 timestamp)",
},
timeMax: {
type: "string",
description: "Upper bound for event start time (RFC3339 timestamp)",
},
maxResults: {
type: "number",
description: "Maximum number of events to return (default: 250)",
default: 250,
},
singleEvents: {
type: "boolean",
description: "Whether to expand recurring events (default: true)",
default: true,
},
orderBy: {
type: "string",
enum: ["startTime", "updated"],
description: "Order of events (default: 'startTime')",
default: "startTime",
},
},
},
},
{
name: "create_event",
description: "Create a new event in Google Calendar",
inputSchema: {
type: "object",
properties: {
calendarId: {
type: "string",
description: "Calendar ID (default: 'primary')",
default: "primary",
},
summary: {
type: "string",
description: "Event title/summary",
},
description: {
type: "string",
description: "Event description",
},
start: {
type: "object",
properties: {
dateTime: {
type: "string",
description: "Start date and time (RFC3339 timestamp)",
},
timeZone: {
type: "string",
description: "Time zone (e.g., 'America/New_York')",
},
},
required: ["dateTime"],
},
end: {
type: "object",
properties: {
dateTime: {
type: "string",
description: "End date and time (RFC3339 timestamp)",
},
timeZone: {
type: "string",
description: "Time zone (e.g., 'America/New_York')",
},
},
required: ["dateTime"],
},
location: {
type: "string",
description: "Event location",
},
attendees: {
type: "array",
items: {
type: "object",
properties: {
email: { type: "string" },
displayName: { type: "string" },
},
required: ["email"],
},
description: "List of attendees",
},
recurrence: {
type: "array",
items: { type: "string" },
description: "Recurrence rules (RRULE format)",
},
},
required: ["summary", "start", "end"],
},
},
{
name: "update_event",
description: "Update an existing event in Google Calendar",
inputSchema: {
type: "object",
properties: {
calendarId: {
type: "string",
description: "Calendar ID (default: 'primary')",
default: "primary",
},
eventId: {
type: "string",
description: "Event ID to update",
},
summary: {
type: "string",
description: "Event title/summary",
},
description: {
type: "string",
description: "Event description",
},
start: {
type: "object",
properties: {
dateTime: {
type: "string",
description: "Start date and time (RFC3339 timestamp)",
},
timeZone: {
type: "string",
description: "Time zone (e.g., 'America/New_York')",
},
},
},
end: {
type: "object",
properties: {
dateTime: {
type: "string",
description: "End date and time (RFC3339 timestamp)",
},
timeZone: {
type: "string",
description: "Time zone (e.g., 'America/New_York')",
},
},
},
location: {
type: "string",
description: "Event location",
},
attendees: {
type: "array",
items: {
type: "object",
properties: {
email: { type: "string" },
displayName: { type: "string" },
},
required: ["email"],
},
description: "List of attendees",
},
},
required: ["eventId"],
},
},
{
name: "delete_event",
description: "Delete an event from Google Calendar",
inputSchema: {
type: "object",
properties: {
calendarId: {
type: "string",
description: "Calendar ID (default: 'primary')",
default: "primary",
},
eventId: {
type: "string",
description: "Event ID to delete",
},
},
required: ["eventId"],
},
},
{
name: "list_calendars",
description: "List available Google Calendars",
inputSchema: {
type: "object",
properties: {
minAccessRole: {
type: "string",
enum: ["freeBusyReader", "owner", "reader", "writer"],
description: "Minimum access role filter",
},
showDeleted: {
type: "boolean",
description: "Include deleted calendars (default: false)",
default: false,
},
showHidden: {
type: "boolean",
description: "Include hidden calendars (default: false)",
default: false,
},
},
},
},
];
// Tool handlers
async function handleListEvents(args: z.infer<typeof ListEventsArgsSchema>) {
try {
const response = await calendar.events.list({
calendarId: args.calendarId,
timeMin: args.timeMin,
timeMax: args.timeMax,
maxResults: args.maxResults,
singleEvents: args.singleEvents,
orderBy: args.orderBy,
});
return {
content: [
{
type: "text",
text: JSON.stringify({
success: true,
events: response.data.items || [],
nextPageToken: response.data.nextPageToken,
summary: `Found ${(response.data.items || []).length} events`,
}, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}, null, 2),
},
],
isError: true,
};
}
}
async function handleCreateEvent(args: z.infer<typeof CreateEventArgsSchema>) {
try {
const event = {
summary: args.summary,
description: args.description,
start: args.start,
end: args.end,
location: args.location,
attendees: args.attendees,
recurrence: args.recurrence,
};
const response = await calendar.events.insert({
calendarId: args.calendarId,
requestBody: event,
});
return {
content: [
{
type: "text",
text: JSON.stringify({
success: true,
event: response.data,
message: `Event "${args.summary}" created successfully`,
}, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}, null, 2),
},
],
isError: true,
};
}
}
async function handleUpdateEvent(args: z.infer<typeof UpdateEventArgsSchema>) {
try {
const updates: any = {};
if (args.summary !== undefined) updates.summary = args.summary;
if (args.description !== undefined) updates.description = args.description;
if (args.start !== undefined) updates.start = args.start;
if (args.end !== undefined) updates.end = args.end;
if (args.location !== undefined) updates.location = args.location;
if (args.attendees !== undefined) updates.attendees = args.attendees;
const response = await calendar.events.patch({
calendarId: args.calendarId,
eventId: args.eventId,
requestBody: updates,
});
return {
content: [
{
type: "text",
text: JSON.stringify({
success: true,
event: response.data,
message: `Event "${args.eventId}" updated successfully`,
}, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}, null, 2),
},
],
isError: true,
};
}
}
async function handleDeleteEvent(args: z.infer<typeof DeleteEventArgsSchema>) {
try {
await calendar.events.delete({
calendarId: args.calendarId,
eventId: args.eventId,
});
return {
content: [
{
type: "text",
text: JSON.stringify({
success: true,
message: `Event "${args.eventId}" deleted successfully`,
}, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}, null, 2),
},
],
isError: true,
};
}
}
async function handleListCalendars(args: z.infer<typeof ListCalendarsArgsSchema>) {
try {
const response = await calendar.calendarList.list({
minAccessRole: args.minAccessRole,
showDeleted: args.showDeleted,
showHidden: args.showHidden,
});
return {
content: [
{
type: "text",
text: JSON.stringify({
success: true,
calendars: response.data.items || [],
summary: `Found ${(response.data.items || []).length} calendars`,
}, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}, null, 2),
},
],
isError: true,
};
}
}
// Register handlers
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "list_events": {
const validatedArgs = ListEventsArgsSchema.parse(args);
return await handleListEvents(validatedArgs);
}
case "create_event": {
const validatedArgs = CreateEventArgsSchema.parse(args);
return await handleCreateEvent(validatedArgs);
}
case "update_event": {
const validatedArgs = UpdateEventArgsSchema.parse(args);
return await handleUpdateEvent(validatedArgs);
}
case "delete_event": {
const validatedArgs = DeleteEventArgsSchema.parse(args);
return await handleDeleteEvent(validatedArgs);
}
case "list_calendars": {
const validatedArgs = ListCalendarsArgsSchema.parse(args);
return await handleListCalendars(validatedArgs);
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`,
},
],
isError: true,
};
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Google Calendar MCP Server running on stdio");
}
if (require.main === module) {
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
}