Google Calendar MCP Server
by thisnick
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import fs from "fs";
import { google } from "googleapis";
import path from "path";
import { OAuth2Client } from 'google-auth-library';
import { CreateEventArgsSchema,
SearchEventsArgsSchema,
ListEventsArgsSchema,
SetCalendarDefaultsArgsSchema,
ListCalendarsArgsSchema,
} from "./types.js";
import { zodToJsonSchema } from "zod-to-json-schema";
import { z } from "zod";
const calendar = google.calendar("v3");
// Store multiple auth instances
const authInstances: { [accountId: string]: any } = {};
interface Settings {
defaultAccountId?: string;
defaultCalendarId?: string;
}
function getSettingsPath() {
return path.join(
path.dirname(new URL(import.meta.url).pathname),
"..",
".gcal-settings.json"
);
}
function loadSettings(): Settings {
try {
return JSON.parse(fs.readFileSync(getSettingsPath(), "utf-8"));
} catch {
return {};
}
}
function saveSettings(settings: Settings) {
fs.writeFileSync(getSettingsPath(), JSON.stringify(settings, null, 2));
}
function getAuthInstance(accountId?: string): OAuth2Client {
if (!accountId) {
const settings = loadSettings();
accountId = settings.defaultAccountId;
if (!accountId) {
const firstAccount = Object.keys(authInstances)[0];
if (!firstAccount) throw new Error("No authenticated accounts found");
return authInstances[firstAccount];
}
}
const auth = authInstances[accountId];
if (!auth) throw new Error(`Account ${accountId} not found`);
return auth;
}
interface ToolCapability {
description: string;
inputSchema: any;
outputSchema: any;
}
interface ServerCapabilities {
resources: {
[mimeType: string]: {
description: string;
};
};
tools: {
[name: string]: ToolCapability;
};
}
// Store tool definitions for reuse
const toolDefinitions = {
create_event: {
description: "Create a new calendar event in Google Calendar. Provide the account ID, event summary, start and end times, and optionally a calendar ID, location, and description.",
inputSchema: zodToJsonSchema(CreateEventArgsSchema),
outputSchema: {
type: "object",
properties: {
content: {
type: "array",
items: {
type: "object",
properties: {
type: { type: "string", enum: ["text"] },
text: { type: "string" }
},
required: ["type", "text"]
}
}
},
required: ["content"]
}
},
search_events: {
description: "Search for calendar events in Google Calendar using a text query. Returns events matching the query text in their title, description, or location.",
inputSchema: zodToJsonSchema(SearchEventsArgsSchema),
outputSchema: {
type: "object",
properties: {
content: {
type: "array",
items: {
type: "object",
properties: {
type: { type: "string", enum: ["text"] },
text: { type: "string" }
},
required: ["type", "text"]
}
}
},
required: ["content"]
}
},
list_events: {
description: "List upcoming calendar events from Google Calendar. Specify the account ID, optional calendar ID, maximum number of results to return, and optional time range filters.",
inputSchema: zodToJsonSchema(ListEventsArgsSchema),
outputSchema: {
type: "object",
properties: {
content: {
type: "array",
items: {
type: "object",
properties: {
type: { type: "string", enum: ["text"] },
text: { type: "string" }
},
required: ["type", "text"]
}
}
},
required: ["content"]
}
},
set_calendar_defaults: {
description: "Set the default Google account and optionally the default calendar to use for calendar operations. These defaults will be used when account or calendar are not explicitly specified.",
inputSchema: zodToJsonSchema(SetCalendarDefaultsArgsSchema),
outputSchema: {
type: "object",
properties: {
content: {
type: "array",
items: {
type: "object",
properties: {
type: { type: "string", enum: ["text"] },
text: { type: "string" }
},
required: ["type", "text"]
}
}
},
required: ["content"]
}
},
list_calendar_accounts: {
description: "List all authenticated Google Calendar accounts that the user has connected to this server. Shows which account is set as default.",
inputSchema: zodToJsonSchema(z.object({})),
outputSchema: {
type: "object",
properties: {
content: {
type: "array",
items: {
type: "object",
properties: {
type: { type: "string", enum: ["text"] },
text: { type: "string" }
},
required: ["type", "text"]
}
}
},
required: ["content"]
}
},
list_calendars: {
description: "List all available calendars for a specific Google account. Requires an account ID and returns calendar names, IDs, and other metadata.",
inputSchema: zodToJsonSchema(ListCalendarsArgsSchema),
outputSchema: {
type: "object",
properties: {
content: {
type: "array",
items: {
type: "object",
properties: {
type: { type: "string", enum: ["text"] },
text: { type: "string" }
},
required: ["type", "text"]
}
}
},
required: ["content"]
}
}
};
// Initialize the MCP server with Google Calendar capabilities
// This server allows models to access and manage Google Calendar events and settings
const server = new McpServer({
name: "Google Calendar",
version: "0.1.0",
protocolVersion: "2.0"
}, {
capabilities: {
tools: toolDefinitions
}
});
// Register tools
server.tool(
"create_event",
"Create a new calendar event in Google Calendar. Provide the account ID, event summary, start and end times, and optionally a calendar ID, location, and description.",
CreateEventArgsSchema.shape,
async ({ summary, description, start, end, calendarId, location, accountId }: z.infer<typeof CreateEventArgsSchema>) => {
// Create a new event in Google Calendar with the provided details
// Returns the created event ID and a confirmation message
const auth = getAuthInstance(accountId);
google.options({ auth });
const settings = loadSettings();
try {
const event = await calendar.events.insert({
calendarId: calendarId || settings.defaultCalendarId || 'primary',
requestBody: {
summary,
description,
start: { dateTime: start },
end: { dateTime: end },
location
}
});
return {
content: [{
type: "text",
text: `Created event: ${event.data.htmlLink}`
}]
};
} catch (error: any) {
throw new Error(`Failed to create event: ${error.message}`);
}
}
);
server.tool(
"search_events",
"Search for calendar events in Google Calendar using a text query. Returns events matching the query text in their title, description, or location.",
SearchEventsArgsSchema.shape,
async ({ query, accountId }: z.infer<typeof SearchEventsArgsSchema>) => {
// Search for events in Google Calendar matching the query string
// Returns a list of matching events with their details
const auth = getAuthInstance(accountId);
google.options({ auth });
try {
const res = await calendar.events.list({
calendarId: 'primary',
q: query,
maxResults: 10
});
const eventList = res.data.items
?.map(event => `${event.summary} (${event.start?.dateTime || event.start?.date})`)
.join("\n");
return {
content: [{
type: "text",
text: `Found ${res.data.items?.length ?? 0} events:\n${eventList}`
}]
};
} catch (error: any) {
throw new Error(`Failed to search events: ${error.message}`);
}
}
);
server.tool(
"list_events",
"List upcoming calendar events from Google Calendar. Specify the account ID, optional calendar ID, maximum number of results to return, and optional time range filters.",
ListEventsArgsSchema.shape,
async ({ accountId, calendarId, maxResults = 10, timeMin, timeMax }: z.infer<typeof ListEventsArgsSchema>) => {
// List upcoming events from the specified calendar
// Returns events with their titles, times, and other details
const auth = getAuthInstance(accountId);
google.options({ auth });
const defaultTimeMin = new Date();
defaultTimeMin.setDate(defaultTimeMin.getDate() - 7);
try {
if (calendarId) {
try {
await calendar.calendars.get({ calendarId });
} catch (error: any) {
throw new Error(`Calendar ${calendarId} not found: ${error.message}`);
}
const params = {
calendarId,
timeMin: timeMin || defaultTimeMin.toISOString(),
timeMax: timeMax || undefined,
maxResults,
singleEvents: true,
orderBy: 'startTime'
};
const res = await calendar.events.list(params);
const eventList = res.data.items
?.map(event => `- ${event.summary || 'Untitled'} (${event.start?.dateTime || event.start?.date})`)
.join("\n");
return {
content: [{
type: "text",
text: `Events for calendar ${calendarId} in account ${accountId}:\n${eventList}`
}]
};
}
const calendarsResponse = await calendar.calendarList.list();
const calendars = calendarsResponse.data.items || [];
let allEvents = [];
for (const cal of calendars) {
if (!cal.id) continue;
const params = {
calendarId: cal.id,
timeMin: timeMin || defaultTimeMin.toISOString(),
timeMax: timeMax || undefined,
maxResults,
singleEvents: true,
orderBy: 'startTime'
};
const res = await calendar.events.list(params);
const events = res.data.items || [];
allEvents.push(...events.map(event => ({
summary: event.summary || 'Untitled',
calendar: cal.summary,
start: event.start?.dateTime || event.start?.date
})));
}
allEvents.sort((a, b) => {
const dateA = a.start ? new Date(a.start).getTime() : 0;
const dateB = b.start ? new Date(b.start).getTime() : 0;
return dateA - dateB;
});
allEvents = allEvents.slice(0, maxResults);
const eventList = allEvents
.map(event => `- [${event.calendar}] ${event.summary} (${event.start})`)
.join("\n");
return {
content: [{
type: "text",
text: `Events across all calendars for account ${accountId}:\n${eventList}`
}]
};
} catch (error: any) {
throw new Error(`Failed to list events: ${error.message}`);
}
}
);
server.tool(
"set_calendar_defaults",
"Set the default Google account and optionally the default calendar to use for calendar operations. These defaults will be used when account or calendar are not explicitly specified.",
SetCalendarDefaultsArgsSchema.shape,
async ({ accountId, calendarId }: z.infer<typeof SetCalendarDefaultsArgsSchema>) => {
// Set the default Google account and calendar to use for operations
// These defaults will be used when parameters are not explicitly provided
if (!authInstances[accountId]) {
throw new Error(`Account ${accountId} not found`);
}
const auth = getAuthInstance(accountId);
google.options({ auth });
try {
if (calendarId) {
try {
await calendar.calendars.get({ calendarId });
} catch (error: any) {
throw new Error(`Calendar ${calendarId} not found: ${error.message}`);
}
}
const settings = loadSettings();
settings.defaultAccountId = accountId;
if (calendarId) settings.defaultCalendarId = calendarId;
saveSettings(settings);
return {
content: [{
type: "text",
text: `Default account set to ${accountId}${calendarId ? ` and calendar set to ${calendarId}` : ''}`
}]
};
} catch (error: any) {
throw new Error(`Failed to set calendar defaults: ${error.message}`);
}
}
);
server.tool(
"list_calendar_accounts",
"List all authenticated Google Calendar accounts that the user has connected to this server. Shows which account is set as default.",
z.object({}).shape,
async () => {
// List all authenticated Google Calendar accounts
// Indicates which account is currently set as the default
try {
const accounts = Object.keys(authInstances).map(accountId => {
const isDefault = loadSettings().defaultAccountId === accountId;
return `${isDefault ? '* ' : '- '}${accountId}`;
}).join('\n');
return {
content: [{
type: "text",
text: accounts ? `Available accounts:\n${accounts}\n(* indicates default account)` : "No accounts configured"
}]
};
} catch (error: any) {
throw new Error(`Failed to list calendar accounts: ${error.message}`);
}
}
);
server.tool(
"list_calendars",
"List all available calendars for a specific Google account. Requires an account ID and returns calendar names, IDs, and other metadata.",
ListCalendarsArgsSchema.shape,
async ({ accountId }: z.infer<typeof ListCalendarsArgsSchema>) => {
// List all calendars available in the specified Google account
// Returns calendar IDs, names, and attributes
const auth = getAuthInstance(accountId);
google.options({ auth });
try {
const calendars = await calendar.calendarList.list();
const defaultCalendarId = loadSettings().defaultCalendarId;
const calendarList = calendars.data.items?.map(cal => {
const isDefault = cal.id === defaultCalendarId;
return `${isDefault ? '* ' : '- '}${cal.summary} (${cal.id})`;
}).join('\n');
return {
content: [{
type: "text",
text: calendarList ? `Available calendars:\n${calendarList}\n(* indicates default calendar)` : "No calendars found"
}]
};
} catch (error: any) {
throw new Error(`Failed to list calendars: ${error.message}`);
}
}
);
// Helper to get tokens path for an account
const getTokensPath = (accountId: string) => {
return path.join(
path.dirname(new URL(import.meta.url).pathname),
"..",
`.gcal-tokens-${accountId}.json`
);
};
// Create OAuth client from credentials file
function getOAuthClient() {
const credentials = JSON.parse(
fs.readFileSync(
path.join(path.dirname(new URL(import.meta.url).pathname), "..", ".client_secret.json"),
"utf-8"
)
);
return new google.auth.OAuth2(
credentials.installed.client_id,
credentials.installed.client_secret,
"urn:ietf:wg:oauth:2.0:oob"
);
}
async function loadCredentialsAndRunServer() {
const tokenFiles = fs.readdirSync(path.join(path.dirname(new URL(import.meta.url).pathname), ".."))
.filter(f => f.startsWith('.gcal-tokens-'));
if (tokenFiles.length === 0) {
throw new Error("No tokens found. Please run with 'auth <account-id>' first.");
}
// Initialize auth for each account
for (const file of tokenFiles) {
const accountId = file.replace('.gcal-tokens-', '').replace('.json', '');
const tokens = JSON.parse(
fs.readFileSync(getTokensPath(accountId), "utf-8")
);
const oAuth2Client = getOAuthClient();
oAuth2Client.setCredentials(tokens);
// Set up token refresh handler
oAuth2Client.on('tokens', (tokens) => {
const allTokens = {
...JSON.parse(fs.readFileSync(getTokensPath(accountId), "utf-8")),
...tokens
};
fs.writeFileSync(getTokensPath(accountId), JSON.stringify(allTokens));
});
authInstances[accountId] = oAuth2Client;
}
const transport = new StdioServerTransport();
await server.connect(transport);
}
// Helper function to get authorization code from user
async function getAuthorizationCode(): Promise<string> {
const readline = await import('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
rl.question('Enter the authorization code: ', (code: string) => {
rl.close();
resolve(code.trim());
});
});
}
async function authenticateAccount(accountId: string) {
const oAuth2Client = getOAuthClient();
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: ['https://www.googleapis.com/auth/calendar'],
prompt: 'consent'
});
console.log(`Please authorize this app by visiting: ${authUrl}`);
const code = await getAuthorizationCode();
const { tokens } = await oAuth2Client.getToken(code);
oAuth2Client.setCredentials(tokens);
fs.writeFileSync(
getTokensPath(accountId),
JSON.stringify(tokens)
);
console.log(`Successfully authenticated ${accountId}`);
return oAuth2Client;
}
if (process.argv[2] === "auth") {
const accountId = process.argv[3];
if (!accountId) {
throw new Error("Please provide an account ID");
}
authenticateAccount(accountId).catch(error => {
console.error("Authentication failed:", error.message);
process.exit(1);
});
} else {
loadCredentialsAndRunServer().catch(error => {
console.error("Server failed:", error.message);
process.exit(1);
});
}