@pablosr/mcp-google-calendar 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Pablo Suarez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,309 @@
1
+ # mcp-google-calendar
2
+
3
+
4
+ A Google Calendar (MCP) server to expose calendar operations as tools for LLM.
5
+
6
+
7
+ ## Table of Contents
8
+
9
+ - [mcp-google-calendar](#mcp-google-calendar)
10
+ - [Table of Contents](#table-of-contents)
11
+ - [Important: Authentication Architecture](#important-authentication-architecture)
12
+ - [Current Implementation](#current-implementation)
13
+ - [Multi-User Scenarios](#multi-user-scenarios)
14
+ - [Setup](#setup)
15
+ - [Prerequisites](#prerequisites)
16
+ - [Step-by-step Configuration](#step-by-step-configuration)
17
+ - [1. Configure Google Cloud Console Project](#1-configure-google-cloud-console-project)
18
+ - [2. Create OAuth 2.0 Credentials](#2-create-oauth-20-credentials)
19
+ - [3. Configure the .env file](#3-configure-the-env-file)
20
+ - [4. Get the refresh token](#4-get-the-refresh-token)
21
+ - [5. Test the configuration](#5-test-the-configuration)
22
+ - [MCP Client Configuration](#mcp-client-configuration)
23
+ - [Usage](#usage)
24
+ - [Testing with MCP Inspector](#testing-with-mcp-inspector)
25
+ - [Security](#security)
26
+ - [Additional Resources](#additional-resources)
27
+ - [Available Tools](#available-tools)
28
+ - [google-create-event](#google-create-event)
29
+ - [google-update-event](#google-update-event)
30
+ - [google-list-events](#google-list-events)
31
+ - [google-search-events](#google-search-events)
32
+ - [google-delete-event](#google-delete-event)
33
+ - [google-list-calendars](#google-list-calendars)
34
+ - [google-get-current-datetime](#google-get-current-datetime)
35
+ - [License](#license)
36
+
37
+
38
+ ## Important: Authentication Architecture
39
+
40
+ **This MCP server is configured to use a single Google account for all operations.** The authentication credentials (client ID, client secret, and refresh token) are stored in environment variables (`.env` file), meaning all users of this MCP server will interact with the same Google Calendar account.
41
+
42
+ ### Current Implementation
43
+ - **Single Account Mode**: One Google account's credentials are configured in the `.env` file
44
+ - **All operations** (create, read, update, delete events) are performed on this single account
45
+ - **Best for**: Personal use, single-user applications, or shared team calendars
46
+
47
+ ### Multi-User Scenarios
48
+ If you need **each user to authenticate with their own Google account**, this server would require modifications:
49
+
50
+ 1. **Add user authentication layer**: Implement a user authentication system (OAuth2, sessions, JWT, etc.)
51
+ 2. **Pass user context**: Modify the MCP tools to use the authenticated user's token instead of the `.env` token
52
+ 3. **Token refresh logic**: Implement per-user token refresh and management
53
+
54
+ > **Note**: The current implementation prioritizes simplicity for personal use.
55
+
56
+
57
+ ## Setup
58
+
59
+ ### Prerequisites
60
+
61
+ 1. A Google account
62
+ 2. Access to Google Cloud Console
63
+ 3. Node.js installed
64
+
65
+ ### Step-by-step Configuration
66
+
67
+ #### 1. Configure Google Cloud Console Project
68
+
69
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/)
70
+ 2. Create a new project or select an existing one
71
+ 3. Enable the [**Google Calendar API**](https://console.cloud.google.com/apis/library/calendar-json.googleapis.com):
72
+ - Go to "APIs & Services" > "Library"
73
+ - Search for "Google Calendar API"
74
+ - Click "Enable"
75
+
76
+ #### 2. Create OAuth 2.0 Credentials
77
+
78
+ 1. Go to "APIs & Services" > "Credentials"
79
+ 2. Click "Create Credentials" > "OAuth 2.0 Client ID"
80
+ 3. If it's your first time, configure the OAuth consent screen:
81
+ - Select "External" (or "Internal" if you have Google Workspace)
82
+ - Complete the basic app information
83
+ - In "Scopes", don't add any scope (we'll do this programmatically)
84
+ - Add your email as a test user
85
+ 4. Create the OAuth 2.0 Client ID:
86
+ - Application type: "Web application"
87
+ - Name: "Google Calendar MCP Server"
88
+ - Authorized redirect URIs: `http://localhost:8080/oauth/callback`
89
+
90
+ #### 3. Configure the .env file
91
+
92
+ 1. Copy the credentials from Google Cloud Console
93
+ 2. Create a `.env` file in the project root:
94
+
95
+ ```bash
96
+ # Google Cloud Console credentials
97
+ GOOGLE_CLIENT_ID=your_client_id_here.apps.googleusercontent.com
98
+ GOOGLE_CLIENT_SECRET=your_client_secret_here
99
+ GOOGLE_REDIRECT_URL=http://localhost:8080/oauth/callback
100
+
101
+ # This will be generated in the next step
102
+ GOOGLE_REFRESH_TOKEN=
103
+ ```
104
+
105
+ #### 4. Get the refresh token
106
+
107
+ Run the setup script:
108
+
109
+ ```bash
110
+ npm run setup:google
111
+ ```
112
+
113
+ This script will:
114
+ 1. Start a temporary server on port 8080
115
+ 2. Show you a URL to authorize the application
116
+ 3. Open that URL in your browser
117
+ 4. After authorizing, give you a `refresh_token`
118
+ 5. Return token `GOOGLE_REFRESH_TOKEN`
119
+
120
+ #### 5. Test the configuration
121
+
122
+ ```bash
123
+ npm run build
124
+ node dist/index.js
125
+ ```
126
+
127
+ If everything is configured correctly, you'll see:
128
+ ```
129
+ ✅ Google Calendar configured correctly
130
+ ```
131
+
132
+ ### MCP Client Configuration
133
+
134
+ Add this to your MCP client configuration (e.g., Claude Desktop config):
135
+
136
+ ```json
137
+ {
138
+ "mcpServers": {
139
+ "google-calendar": {
140
+ "command": "npx",
141
+ //WIP: "args": ["mcp-google-calendar"],
142
+ "env": {
143
+ "GOOGLE_CLIENT_ID": "<your-client-id>",
144
+ "GOOGLE_CLIENT_SECRET": "<your-client-secret>",
145
+ "GOOGLE_REDIRECT_URL": "http://localhost:8080/oauth/callback",
146
+ "GOOGLE_REFRESH_TOKEN": "<your-refresh-token>"
147
+ }
148
+ }
149
+ }
150
+ }
151
+ ```
152
+
153
+ ## Usage
154
+
155
+ 1. Compile TypeScript to JavaScript:
156
+ ```bash
157
+ npm run build
158
+ ```
159
+
160
+ 2. Run the MCP server:
161
+ ```bash
162
+ node dist/index.js
163
+ ```
164
+
165
+ ## Testing with MCP Inspector
166
+
167
+ You can test and debug this MCP server using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
168
+
169
+ 1. Make sure you have built the project:
170
+ ```bash
171
+ npm run build
172
+ ```
173
+
174
+ 2. Set up your environment variables in `.env` file:
175
+ ```bash
176
+ GOOGLE_CLIENT_ID=your-client-id
177
+ GOOGLE_CLIENT_SECRET=your-client-secret
178
+ GOOGLE_REDIRECT_URL=http://localhost:8080/oauth/callback
179
+ GOOGLE_REFRESH_TOKEN=your-refresh-token
180
+ ```
181
+
182
+ 3. Update `mcp-inspector-config.json` with your project path:
183
+ ```json
184
+ {
185
+ "mcpServers": {
186
+ "mcp-google-calendar": {
187
+ "command": "node",
188
+ "args": ["dist/index.js"],
189
+ "env": {
190
+ "GOOGLE_CLIENT_ID": "${GOOGLE_CLIENT_ID}",
191
+ "GOOGLE_CLIENT_SECRET": "${GOOGLE_CLIENT_SECRET}",
192
+ "GOOGLE_REDIRECT_URL": "${GOOGLE_REDIRECT_URL}",
193
+ "GOOGLE_REFRESH_TOKEN": "${GOOGLE_REFRESH_TOKEN}"
194
+ }
195
+ }
196
+ }
197
+ }
198
+ ```
199
+
200
+ 4. Run the MCP Inspector:
201
+ ```bash
202
+ npx @modelcontextprotocol/inspector --config mcp-inspector-config.json
203
+ ```
204
+
205
+ 1. Open your browser to the URL shown in the terminal (default should be http://localhost:6277) to interact with the MCP server through the Inspector UI.
206
+
207
+
208
+ ## Security
209
+
210
+ - **Never** share your `client_secret` or `refresh_token`
211
+ - Add `.env` to your `.gitignore` (already included)
212
+ - Tokens have limited permissions only for Google Calendar
213
+
214
+ ## Additional Resources
215
+
216
+ - [Google Calendar API Documentation](https://developers.google.com/calendar/api)
217
+ - [OAuth 2.0 for Web Server Applications](https://developers.google.com/identity/protocols/oauth2/web-server)
218
+ - [Google Cloud Console](https://console.cloud.google.com/)
219
+ - [Model Context Protocol Documentation](https://modelcontextprotocol.io)
220
+
221
+ ## Available Tools
222
+
223
+ ### google-create-event
224
+
225
+ Creates a new calendar event in Google Calendar.
226
+
227
+ Parameters:
228
+ - `title`: String - Event title/summary
229
+ - `start`: DateTime string - Event start time (ISO 8601 format)
230
+ - `end`: DateTime string - Event end time (ISO 8601 format)
231
+ - `description`: String (optional) - Event description
232
+ - `location`: String (optional) - Event location
233
+ - `calendarId`: String (optional) - Calendar ID (defaults to 'primary')
234
+
235
+ Returns:
236
+ - The unique ID and details of the created event
237
+
238
+ ### google-update-event
239
+
240
+ Updates an existing event in Google Calendar.
241
+
242
+ Parameters:
243
+ - `eventId`: String - The unique ID of the event to update
244
+ - `title`: String (optional) - New event title/summary
245
+ - `start`: DateTime string (optional) - New event start time (ISO 8601 format)
246
+ - `end`: DateTime string (optional) - New event end time (ISO 8601 format)
247
+ - `description`: String (optional) - New event description
248
+ - `location`: String (optional) - New event location
249
+ - `calendarId`: String (optional) - Calendar ID (defaults to 'primary')
250
+
251
+ Returns:
252
+ - The updated event details
253
+
254
+ ### google-list-events
255
+
256
+ Lists events within a specified timeframe from Google Calendar.
257
+
258
+ Parameters:
259
+ - `timeMin`: DateTime string (optional) - Start of the timeframe (ISO 8601 format)
260
+ - `timeMax`: DateTime string (optional) - End of the timeframe (ISO 8601 format)
261
+ - `calendarId`: String (optional) - Calendar ID (defaults to 'primary')
262
+ - `maxResults`: Number (optional) - Maximum number of events to return (default: 10)
263
+
264
+ Returns:
265
+ - A list of events that fall within the given timeframe
266
+
267
+ ### google-search-events
268
+
269
+ Searches for events in Google Calendar by text query.
270
+
271
+ Parameters:
272
+ - `query`: String - Search query
273
+ - `calendarId`: String (optional) - Calendar ID (defaults to 'primary')
274
+ - `maxResults`: Number (optional) - Maximum number of events to return (default: 10)
275
+
276
+ Returns:
277
+ - A list of events matching the search query
278
+
279
+ ### google-delete-event
280
+
281
+ Deletes an event from Google Calendar.
282
+
283
+ Parameters:
284
+ - `eventId`: String - The unique ID of the event to delete
285
+ - `calendarId`: String (optional) - Calendar ID (defaults to 'primary')
286
+
287
+ Returns:
288
+ - Confirmation of deletion
289
+
290
+ ### google-list-calendars
291
+
292
+ Lists all calendars available to the user.
293
+
294
+ Returns:
295
+ - A list of calendars with their IDs and names
296
+
297
+ ### google-get-current-datetime
298
+
299
+ Gets the current date and time in ISO 8601 format. Useful for references when creating or searching for events.
300
+
301
+ Parameters:
302
+ - `timezone`: String (optional) - Timezone in IANA format (e.g., America/New_York, Europe/Madrid). Defaults to 'Atlantic/Canary'.
303
+
304
+ Returns:
305
+ - Current date and time information including ISO 8601 format, timestamp, local time, and timezone details
306
+
307
+ ## License
308
+
309
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { GoogleCalendarClient } from "./tools/google-calendar.js";
6
+ import { registerGoogleCalendarTools } from "./tools/google-calendar-mcp.js";
7
+ const server = new McpServer({
8
+ name: "google-calendar-mcp",
9
+ version: "1.0.0",
10
+ });
11
+ async function main() {
12
+ console.error('Configurando Google Calendar API...');
13
+ // Verificar que las credenciales de Google estén configuradas
14
+ if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) {
15
+ console.error('Error: GOOGLE_CLIENT_ID y GOOGLE_CLIENT_SECRET son requeridos');
16
+ process.exit(1);
17
+ }
18
+ const googleClient = new GoogleCalendarClient({
19
+ clientId: process.env.GOOGLE_CLIENT_ID,
20
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
21
+ redirectUrl: process.env.GOOGLE_REDIRECT_URL || 'http://localhost:8080/oauth/callback',
22
+ refreshToken: process.env.GOOGLE_REFRESH_TOKEN,
23
+ });
24
+ // Si no tenemos refresh token, mostrar URL de autorización
25
+ if (!process.env.GOOGLE_REFRESH_TOKEN) {
26
+ console.error('\n🔑 AUTORIZACIÓN REQUERIDA');
27
+ console.error('Visita la siguiente URL para autorizar la aplicación:');
28
+ console.error(googleClient.getAuthUrl());
29
+ console.error('\nDespués de autorizar, agrega el refresh_token al archivo .env como GOOGLE_REFRESH_TOKEN=...\n');
30
+ process.exit(1);
31
+ }
32
+ registerGoogleCalendarTools(googleClient, server);
33
+ console.log('Google Calendar configurado correctamente');
34
+ // Start receiving messages on stdin and sending messages on stdout
35
+ const transport = new StdioServerTransport();
36
+ await server.connect(transport);
37
+ }
38
+ main().catch((error) => {
39
+ console.error('Error al iniciar el servidor:', error);
40
+ process.exit(1);
41
+ });
@@ -0,0 +1,16 @@
1
+ import { registerGoogleListCalendars } from './google-list-calendars.js';
2
+ import { registerGoogleListEvents } from './google-list-events.js';
3
+ import { registerGoogleCreateEvent } from './google-create-event.js';
4
+ import { registerGoogleUpdateEvent } from './google-update-event.js';
5
+ import { registerGoogleDeleteEvent } from './google-delete-event.js';
6
+ import { registerGoogleSearchEvents } from './google-search-events.js';
7
+ import { registerGoogleGetCurrentDatetime } from './google-get-current-datetime.js';
8
+ export function registerGoogleCalendarTools(googleClient, server) {
9
+ registerGoogleListCalendars(googleClient, server);
10
+ registerGoogleListEvents(googleClient, server);
11
+ registerGoogleCreateEvent(googleClient, server);
12
+ registerGoogleUpdateEvent(googleClient, server);
13
+ registerGoogleDeleteEvent(googleClient, server);
14
+ registerGoogleSearchEvents(googleClient, server);
15
+ registerGoogleGetCurrentDatetime(googleClient, server);
16
+ }
@@ -0,0 +1,153 @@
1
+ import { google } from 'googleapis';
2
+ export class GoogleCalendarClient {
3
+ auth;
4
+ calendar;
5
+ constructor(config) {
6
+ this.auth = new google.auth.OAuth2(config.clientId, config.clientSecret, config.redirectUrl);
7
+ if (config.refreshToken) {
8
+ this.auth.setCredentials({
9
+ refresh_token: config.refreshToken,
10
+ });
11
+ }
12
+ this.calendar = google.calendar({ version: 'v3', auth: this.auth });
13
+ }
14
+ /**
15
+ * Genera URL de autorización para obtener el token inicial
16
+ */
17
+ getAuthUrl() {
18
+ const scopes = [
19
+ 'https://www.googleapis.com/auth/calendar',
20
+ 'https://www.googleapis.com/auth/calendar.events'
21
+ ];
22
+ return this.auth.generateAuthUrl({
23
+ access_type: 'offline',
24
+ scope: scopes,
25
+ });
26
+ }
27
+ /**
28
+ * Intercambia el código de autorización por tokens
29
+ */
30
+ async getTokens(code) {
31
+ const { tokens } = await this.auth.getToken(code);
32
+ this.auth.setCredentials(tokens);
33
+ return tokens;
34
+ }
35
+ /**
36
+ * Lista todos los calendarios del usuario
37
+ */
38
+ async listCalendars() {
39
+ try {
40
+ const response = await this.calendar.calendarList.list();
41
+ return response.data.items || [];
42
+ }
43
+ catch (error) {
44
+ console.error('Error al listar calendarios:', error);
45
+ throw error;
46
+ }
47
+ }
48
+ /**
49
+ * Lista eventos de un calendario específico
50
+ */
51
+ async listEvents(calendarId = 'primary', timeMin, timeMax, maxResults = 10) {
52
+ try {
53
+ const response = await this.calendar.events.list({
54
+ calendarId,
55
+ timeMin: timeMin || new Date().toISOString(),
56
+ timeMax,
57
+ maxResults,
58
+ singleEvents: true,
59
+ orderBy: 'startTime',
60
+ });
61
+ return response.data.items || [];
62
+ }
63
+ catch (error) {
64
+ console.error('Error al listar eventos:', error);
65
+ throw error;
66
+ }
67
+ }
68
+ /**
69
+ * Crea un nuevo evento
70
+ */
71
+ async createEvent(calendarId = 'primary', event) {
72
+ try {
73
+ const response = await this.calendar.events.insert({
74
+ calendarId,
75
+ requestBody: event,
76
+ });
77
+ return response.data;
78
+ }
79
+ catch (error) {
80
+ console.error('Error al crear evento:', error);
81
+ throw error;
82
+ }
83
+ }
84
+ /**
85
+ * Actualiza un evento existente
86
+ */
87
+ async updateEvent(calendarId = 'primary', eventId, event) {
88
+ try {
89
+ const response = await this.calendar.events.update({
90
+ calendarId,
91
+ eventId,
92
+ requestBody: event,
93
+ });
94
+ return response.data;
95
+ }
96
+ catch (error) {
97
+ console.error('Error al actualizar evento:', error);
98
+ throw error;
99
+ }
100
+ }
101
+ /**
102
+ * Elimina un evento
103
+ */
104
+ async deleteEvent(calendarId = 'primary', eventId) {
105
+ try {
106
+ await this.calendar.events.delete({
107
+ calendarId,
108
+ eventId,
109
+ });
110
+ }
111
+ catch (error) {
112
+ console.error('Error al eliminar evento:', error);
113
+ throw error;
114
+ }
115
+ }
116
+ /**
117
+ * Busca eventos por texto
118
+ */
119
+ async searchEvents(calendarId = 'primary', query, maxResults = 10) {
120
+ try {
121
+ const response = await this.calendar.events.list({
122
+ calendarId,
123
+ q: query,
124
+ maxResults,
125
+ singleEvents: true,
126
+ orderBy: 'startTime',
127
+ });
128
+ return response.data.items || [];
129
+ }
130
+ catch (error) {
131
+ console.error('Error al buscar eventos:', error);
132
+ throw error;
133
+ }
134
+ }
135
+ }
136
+ /**
137
+ * Función helper para crear eventos con formato simplificado
138
+ */
139
+ export function createSimpleEvent(title, start, end, description, location) {
140
+ return {
141
+ summary: title,
142
+ description,
143
+ location,
144
+ start: {
145
+ dateTime: start,
146
+ timeZone: 'America/Mexico_City', // Ajusta según tu zona horaria
147
+ },
148
+ end: {
149
+ dateTime: end,
150
+ timeZone: 'America/Mexico_City',
151
+ },
152
+ };
153
+ }
@@ -0,0 +1,49 @@
1
+ import { createSimpleEvent } from './google-calendar.js';
2
+ import { z } from 'zod';
3
+ export function registerGoogleCreateEvent(googleClient, server) {
4
+ server.tool('google_create_event', 'Crea un nuevo evento en Google Calendar', {
5
+ calendarId: z.string().optional().describe('ID del calendario (por defecto: primary)'),
6
+ title: z.string().describe('Título del evento'),
7
+ start: z.string().describe('Fecha/hora de inicio en formato ISO (ej: 2023-12-01T10:00:00Z)'),
8
+ end: z.string().describe('Fecha/hora de fin en formato ISO'),
9
+ description: z.string().optional().describe('Descripción del evento (opcional)'),
10
+ location: z.string().optional().describe('Ubicación del evento (opcional)'),
11
+ }, async (args) => {
12
+ try {
13
+ const { calendarId = 'primary', title, start, end, description, location, } = args;
14
+ if (!title || !start || !end) {
15
+ return {
16
+ content: [
17
+ {
18
+ type: 'text',
19
+ text: 'Error: title, start y end son requeridos',
20
+ },
21
+ ],
22
+ isError: true,
23
+ };
24
+ }
25
+ const event = createSimpleEvent(title, start, end, description, location);
26
+ const createdEvent = await googleClient.createEvent(calendarId, event);
27
+ return {
28
+ content: [
29
+ {
30
+ type: 'text',
31
+ text: `Evento creado exitosamente:\nID: ${createdEvent.id}\nTítulo: ${createdEvent.summary}\nInicio: ${createdEvent.start?.dateTime}\nFin: ${createdEvent.end?.dateTime}`,
32
+ },
33
+ ],
34
+ };
35
+ }
36
+ catch (error) {
37
+ const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
38
+ return {
39
+ content: [
40
+ {
41
+ type: 'text',
42
+ text: `Error al crear evento: ${errorMessage}`,
43
+ },
44
+ ],
45
+ isError: true,
46
+ };
47
+ }
48
+ });
49
+ }
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ export function registerGoogleDeleteEvent(googleClient, server) {
3
+ server.tool('google_delete_event', 'Elimina un evento de Google Calendar', {
4
+ calendarId: z.string().optional().describe('ID del calendario (por defecto: primary)'),
5
+ eventId: z.string().describe('ID del evento a eliminar'),
6
+ }, async (args) => {
7
+ try {
8
+ const { calendarId = 'primary', eventId } = args;
9
+ if (!eventId) {
10
+ return {
11
+ content: [
12
+ {
13
+ type: 'text',
14
+ text: 'Error: eventId es requerido',
15
+ },
16
+ ],
17
+ isError: true,
18
+ };
19
+ }
20
+ await googleClient.deleteEvent(calendarId, eventId);
21
+ return {
22
+ content: [
23
+ {
24
+ type: 'text',
25
+ text: `Evento ${eventId} eliminado exitosamente`,
26
+ },
27
+ ],
28
+ };
29
+ }
30
+ catch (error) {
31
+ const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
32
+ return {
33
+ content: [
34
+ {
35
+ type: 'text',
36
+ text: `Error al eliminar evento: ${errorMessage}`,
37
+ },
38
+ ],
39
+ isError: true,
40
+ };
41
+ }
42
+ });
43
+ }
@@ -0,0 +1,88 @@
1
+ import { z } from 'zod';
2
+ export function registerGoogleGetCurrentDatetime(googleClient, server) {
3
+ server.tool('google_get_current_datetime', 'Obtiene la fecha y hora actual en formato ISO 8601. Útil para referencias temporales al crear o buscar eventos.', {
4
+ timezone: z.string().optional().describe('Zona horaria en formato IANA (ej: America/New_York, Europe/Madrid). Por defecto usa Atlantic/Canary.'),
5
+ }, async (args) => {
6
+ try {
7
+ const { timezone = 'Atlantic/Canary' } = args;
8
+ const now = new Date();
9
+ // Información básica
10
+ const isoString = now.toISOString();
11
+ const timestamp = now.getTime();
12
+ // Si se proporciona una zona horaria, formatear en esa zona
13
+ let localTime;
14
+ let formattedTime;
15
+ try {
16
+ localTime = now.toLocaleString('es-ES', {
17
+ timeZone: timezone,
18
+ year: 'numeric',
19
+ month: '2-digit',
20
+ day: '2-digit',
21
+ hour: '2-digit',
22
+ minute: '2-digit',
23
+ second: '2-digit',
24
+ hour12: false,
25
+ });
26
+ formattedTime = now.toLocaleString('es-ES', {
27
+ timeZone: timezone,
28
+ weekday: 'long',
29
+ year: 'numeric',
30
+ month: 'long',
31
+ day: 'numeric',
32
+ hour: '2-digit',
33
+ minute: '2-digit',
34
+ second: '2-digit',
35
+ hour12: false,
36
+ });
37
+ }
38
+ catch {
39
+ return {
40
+ content: [
41
+ {
42
+ type: 'text',
43
+ text: `Error: Zona horaria inválida '${timezone}'. Usa formato IANA (ej: America/New_York, Europe/Madrid, Atlantic/Canary)`,
44
+ },
45
+ ],
46
+ isError: true,
47
+ };
48
+ }
49
+ const result = {
50
+ iso8601: isoString,
51
+ timestamp: timestamp,
52
+ timezone: timezone,
53
+ localTime: localTime,
54
+ formatted: formattedTime,
55
+ unix: Math.floor(timestamp / 1000),
56
+ date: {
57
+ year: now.getUTCFullYear(),
58
+ month: now.getUTCMonth() + 1,
59
+ day: now.getUTCDate(),
60
+ },
61
+ time: {
62
+ hour: now.getUTCHours(),
63
+ minute: now.getUTCMinutes(),
64
+ second: now.getUTCSeconds(),
65
+ },
66
+ };
67
+ return {
68
+ content: [
69
+ {
70
+ type: 'text',
71
+ text: JSON.stringify(result, null, 2),
72
+ },
73
+ ],
74
+ };
75
+ }
76
+ catch (error) {
77
+ return {
78
+ content: [
79
+ {
80
+ type: 'text',
81
+ text: `Error al obtener la fecha y hora actual: ${error instanceof Error ? error.message : String(error)}`,
82
+ },
83
+ ],
84
+ isError: true,
85
+ };
86
+ }
87
+ });
88
+ }
@@ -0,0 +1,34 @@
1
+ export function registerGoogleListCalendars(googleClient, server) {
2
+ server.tool('google_list_calendars', 'Lista todos los calendarios disponibles en Google Calendar', async () => {
3
+ try {
4
+ const calendars = await googleClient.listCalendars();
5
+ const data = calendars.map((cal) => ({
6
+ id: cal.id,
7
+ summary: cal.summary,
8
+ description: cal.description,
9
+ timeZone: cal.timeZone,
10
+ accessRole: cal.accessRole,
11
+ }));
12
+ return {
13
+ content: [
14
+ {
15
+ type: 'text',
16
+ text: JSON.stringify(data, null, 2),
17
+ },
18
+ ],
19
+ };
20
+ }
21
+ catch (error) {
22
+ const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
23
+ return {
24
+ content: [
25
+ {
26
+ type: 'text',
27
+ text: `Error al listar calendarios: ${errorMessage}`,
28
+ },
29
+ ],
30
+ isError: true,
31
+ };
32
+ }
33
+ });
34
+ }
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ export function registerGoogleListEvents(googleClient, server) {
3
+ server.tool('google_list_events', 'Lista eventos de un calendario de Google Calendar', {
4
+ calendarId: z.string().optional().describe('ID del calendario (por defecto: primary)'),
5
+ timeMin: z.string().optional().describe('Fecha/hora mínima en formato ISO (ej: 2023-12-01T00:00:00Z)'),
6
+ timeMax: z.string().optional().describe('Fecha/hora máxima en formato ISO'),
7
+ maxResults: z.number().optional().describe('Número máximo de resultados (por defecto: 10)'),
8
+ }, async (args) => {
9
+ try {
10
+ const { calendarId = 'primary', timeMin, timeMax, maxResults = 10, } = args;
11
+ const events = await googleClient.listEvents(calendarId, timeMin, timeMax, maxResults);
12
+ const data = events.map((event) => ({
13
+ id: event.id,
14
+ summary: event.summary,
15
+ description: event.description,
16
+ location: event.location,
17
+ start: event.start,
18
+ end: event.end,
19
+ status: event.status,
20
+ }));
21
+ return {
22
+ content: [
23
+ {
24
+ type: 'text',
25
+ text: JSON.stringify(data, null, 2),
26
+ },
27
+ ],
28
+ };
29
+ }
30
+ catch (error) {
31
+ const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
32
+ return {
33
+ content: [
34
+ {
35
+ type: 'text',
36
+ text: `Error al listar eventos: ${errorMessage}`,
37
+ },
38
+ ],
39
+ isError: true,
40
+ };
41
+ }
42
+ });
43
+ }
@@ -0,0 +1,52 @@
1
+ import { z } from 'zod';
2
+ export function registerGoogleSearchEvents(googleClient, server) {
3
+ server.tool('google_search_events', 'Busca eventos en Google Calendar por texto', {
4
+ calendarId: z.string().optional().describe('ID del calendario (por defecto: primary)'),
5
+ query: z.string().describe('Texto a buscar en los eventos'),
6
+ maxResults: z.number().optional().describe('Número máximo de resultados (por defecto: 10)'),
7
+ }, async (args) => {
8
+ try {
9
+ const { calendarId = 'primary', query, maxResults = 10, } = args;
10
+ if (!query) {
11
+ return {
12
+ content: [
13
+ {
14
+ type: 'text',
15
+ text: 'Error: query es requerido',
16
+ },
17
+ ],
18
+ isError: true,
19
+ };
20
+ }
21
+ const events = await googleClient.searchEvents(calendarId, query, maxResults);
22
+ const data = events.map((event) => ({
23
+ id: event.id,
24
+ summary: event.summary,
25
+ description: event.description,
26
+ location: event.location,
27
+ start: event.start,
28
+ end: event.end,
29
+ }));
30
+ return {
31
+ content: [
32
+ {
33
+ type: 'text',
34
+ text: JSON.stringify(data, null, 2),
35
+ },
36
+ ],
37
+ };
38
+ }
39
+ catch (error) {
40
+ const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
41
+ return {
42
+ content: [
43
+ {
44
+ type: 'text',
45
+ text: `Error al buscar eventos: ${errorMessage}`,
46
+ },
47
+ ],
48
+ isError: true,
49
+ };
50
+ }
51
+ });
52
+ }
@@ -0,0 +1,71 @@
1
+ import { z } from 'zod';
2
+ export function registerGoogleUpdateEvent(googleClient, server) {
3
+ server.tool('google_update_event', 'Actualiza un evento existente en Google Calendar', {
4
+ calendarId: z.string().optional().describe('ID del calendario (por defecto: primary)'),
5
+ eventId: z.string().describe('ID del evento a actualizar'),
6
+ title: z.string().optional().describe('Nuevo título del evento'),
7
+ start: z.string().optional().describe('Nueva fecha/hora de inicio en formato ISO (ej: 2023-12-01T10:00:00Z)'),
8
+ end: z.string().optional().describe('Nueva fecha/hora de fin en formato ISO'),
9
+ description: z.string().optional().describe('Nueva descripción del evento'),
10
+ location: z.string().optional().describe('Nueva ubicación del evento'),
11
+ }, async (args) => {
12
+ try {
13
+ const { calendarId = 'primary', eventId, title, start, end, description, location, } = args;
14
+ if (!eventId) {
15
+ return {
16
+ content: [
17
+ {
18
+ type: 'text',
19
+ text: 'Error: eventId es requerido',
20
+ },
21
+ ],
22
+ isError: true,
23
+ };
24
+ }
25
+ // Construir el objeto de evento con solo los campos proporcionados
26
+ const event = {};
27
+ if (title !== undefined) {
28
+ event.summary = title;
29
+ }
30
+ if (description !== undefined) {
31
+ event.description = description;
32
+ }
33
+ if (location !== undefined) {
34
+ event.location = location;
35
+ }
36
+ if (start !== undefined) {
37
+ event.start = {
38
+ dateTime: start,
39
+ timeZone: 'America/Mexico_City',
40
+ };
41
+ }
42
+ if (end !== undefined) {
43
+ event.end = {
44
+ dateTime: end,
45
+ timeZone: 'America/Mexico_City',
46
+ };
47
+ }
48
+ const updatedEvent = await googleClient.updateEvent(calendarId, eventId, event);
49
+ return {
50
+ content: [
51
+ {
52
+ type: 'text',
53
+ text: `Evento actualizado exitosamente:\nID: ${updatedEvent.id}\nTítulo: ${updatedEvent.summary}\nInicio: ${updatedEvent.start?.dateTime}\nFin: ${updatedEvent.end?.dateTime}`,
54
+ },
55
+ ],
56
+ };
57
+ }
58
+ catch (error) {
59
+ const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
60
+ return {
61
+ content: [
62
+ {
63
+ type: 'text',
64
+ text: `Error al actualizar evento: ${errorMessage}`,
65
+ },
66
+ ],
67
+ isError: true,
68
+ };
69
+ }
70
+ });
71
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@pablosr/mcp-google-calendar",
3
+ "description": "A Google Calendar (MCP) server to expose calendar operations as tools for LLM.",
4
+ "version": "1.0.0",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "mcp-google-calendar": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc && shx chmod +x dist/*.js",
15
+ "watch": "tsc --watch",
16
+ "lint": "eslint src/",
17
+ "lint:fix": "eslint src/ --fix",
18
+ "format": "prettier --write src/.",
19
+ "format:check": "prettier --check src/.",
20
+ "setup:google": "node scripts/setup-google-oauth.js"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/PabloSR06/mcp-google-calendar.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/PabloSR06/mcp-google-calendar/issues"
28
+ },
29
+ "homepage": "https://github.com/PabloSR06/mcp-google-calendar#readme",
30
+ "author": "",
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@google-cloud/local-auth": "^3.0.1",
34
+ "@modelcontextprotocol/sdk": "^1.13.0",
35
+ "dotenv": "^16.5.0",
36
+ "googleapis": "^162.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@eslint/js": "^9.29.0",
40
+ "@types/node": "^24.0.3",
41
+ "eslint": "^9.29.0",
42
+ "eslint-config-prettier": "^10.1.5",
43
+ "prettier": "3.5.3",
44
+ "shx": "^0.4.0",
45
+ "typescript": "^5.8.3",
46
+ "typescript-eslint": "^8.34.1"
47
+ }
48
+ }