@pablosr/mcp-google-calendar 1.0.2 → 1.1.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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # mcp-google-calendar
2
2
 
3
3
 
4
- A Google Calendar (MCP) server to expose calendar operations as tools for LLM.
4
+ A Google Calendar and Google Tasks (MCP) server to expose calendar and task operations as tools for LLM.
5
5
 
6
6
 
7
7
  ## Table of Contents
@@ -22,27 +22,35 @@ A Google Calendar (MCP) server to expose calendar operations as tools for LLM.
22
22
  - [MCP Client Configuration](#mcp-client-configuration)
23
23
  - [Usage](#usage)
24
24
  - [Testing with MCP Inspector](#testing-with-mcp-inspector)
25
+ - [Timezone Configuration](#timezone-configuration)
25
26
  - [Security](#security)
26
27
  - [Additional Resources](#additional-resources)
27
28
  - [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)
29
+ - [Calendar Tools](#calendar-tools)
30
+ - [calendar-create-event](#calendar-create-event)
31
+ - [calendar-update-event](#calendar-update-event)
32
+ - [calendar-list-events](#calendar-list-events)
33
+ - [calendar-search-events](#calendar-search-events)
34
+ - [calendar-delete-event](#calendar-delete-event)
35
+ - [calendar-list-calendars](#calendar-list-calendars)
36
+ - [calendar-get-current-datetime](#calendar-get-current-datetime)
37
+ - [Tasks Tools](#tasks-tools)
38
+ - [tasks-list-task-lists](#tasks-list-task-lists)
39
+ - [tasks-list-tasks](#tasks-list-tasks)
40
+ - [tasks-create-task](#tasks-create-task)
41
+ - [tasks-update-task](#tasks-update-task)
42
+ - [tasks-delete-task](#tasks-delete-task)
35
43
  - [License](#license)
36
44
 
37
45
 
38
46
  ## Important: Authentication Architecture
39
47
 
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.
48
+ **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 and Google Tasks account.
41
49
 
42
50
  ### Current Implementation
43
51
  - **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
52
+ - **All operations** (create, read, update, delete events and tasks) are performed on this single account
53
+ - **Best for**: Personal use, single-user applications, or shared team calendars and task lists
46
54
 
47
55
  ### Multi-User Scenarios
48
56
  If you need **each user to authenticate with their own Google account**, this server would require modifications:
@@ -68,10 +76,9 @@ If you need **each user to authenticate with their own Google account**, this se
68
76
 
69
77
  1. Go to [Google Cloud Console](https://console.cloud.google.com/)
70
78
  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"
79
+ 3. Enable the required APIs:
80
+ - **Google Calendar API**: Go to "APIs & Services" > "Library", search for "Google Calendar API" and click "Enable"
81
+ - **Google Tasks API**: Go to "APIs & Services" > "Library", search for "Google Tasks API" and click "Enable"
75
82
 
76
83
  #### 2. Create OAuth 2.0 Credentials
77
84
 
@@ -96,10 +103,12 @@ If you need **each user to authenticate with their own Google account**, this se
96
103
  # Google Cloud Console credentials
97
104
  GOOGLE_CLIENT_ID=your_client_id_here.apps.googleusercontent.com
98
105
  GOOGLE_CLIENT_SECRET=your_client_secret_here
99
- GOOGLE_REDIRECT_URL=http://localhost:8080/oauth/callback
100
106
 
101
107
  # This will be generated in the next step
102
108
  GOOGLE_REFRESH_TOKEN=
109
+
110
+ # Default timezone for calendar operations
111
+ DEFAULT_TIMEZONE=Atlantic/Canary
103
112
  ```
104
113
 
105
114
  #### 4. Get the refresh token
@@ -138,8 +147,8 @@ Add this to your MCP client configuration (e.g., Claude Desktop config):
138
147
  "env": {
139
148
  "GOOGLE_CLIENT_ID": "<your-client-id>",
140
149
  "GOOGLE_CLIENT_SECRET": "<your-client-secret>",
141
- "GOOGLE_REDIRECT_URL": "http://localhost:8080/oauth/callback",
142
- "GOOGLE_REFRESH_TOKEN": "<your-refresh-token>"
150
+ "GOOGLE_REFRESH_TOKEN": "<your-refresh-token>",
151
+ "DEFAULT_TIMEZONE": "Atlantic/Canary"
143
152
  }
144
153
  }
145
154
  }
@@ -171,8 +180,8 @@ npm run build
171
180
  ```bash
172
181
  GOOGLE_CLIENT_ID=your-client-id
173
182
  GOOGLE_CLIENT_SECRET=your-client-secret
174
- GOOGLE_REDIRECT_URL=http://localhost:8080/oauth/callback
175
183
  GOOGLE_REFRESH_TOKEN=your-refresh-token
184
+ DEFAULT_TIMEZONE=Atlantic/Canary
176
185
  ```
177
186
 
178
187
  3. Update `mcp-inspector-config.json` with your project path:
@@ -185,8 +194,8 @@ GOOGLE_REFRESH_TOKEN=your-refresh-token
185
194
  "env": {
186
195
  "GOOGLE_CLIENT_ID": "${GOOGLE_CLIENT_ID}",
187
196
  "GOOGLE_CLIENT_SECRET": "${GOOGLE_CLIENT_SECRET}",
188
- "GOOGLE_REDIRECT_URL": "${GOOGLE_REDIRECT_URL}",
189
- "GOOGLE_REFRESH_TOKEN": "${GOOGLE_REFRESH_TOKEN}"
197
+ "GOOGLE_REFRESH_TOKEN": "${GOOGLE_REFRESH_TOKEN}",
198
+ "DEFAULT_TIMEZONE": "${DEFAULT_TIMEZONE}"
190
199
  }
191
200
  }
192
201
  }
@@ -201,22 +210,44 @@ npx @modelcontextprotocol/inspector --config mcp-inspector-config.json
201
210
  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.
202
211
 
203
212
 
213
+ ## Timezone Configuration
214
+
215
+ This MCP server supports flexible timezone configuration:
216
+
217
+ 1. **Default Timezone**: Set `DEFAULT_TIMEZONE` in your `.env` file using IANA timezone format (e.g., `Atlantic/Canary`, `America/New_York`, `Asia/Tokyo`)
218
+ 2. **Per-Operation Override**: When creating or updating events, you can specify a different timezone for that specific operation
219
+ 3. **Fallback**: If no timezone is configured, the server defaults to `Atlantic/Canary`
220
+
221
+ **Timezone Priority:**
222
+ 1. Timezone parameter passed to the tool (highest priority)
223
+ 2. `DEFAULT_TIMEZONE` environment variable from `.env`
224
+ 3. Hardcoded default: `Atlantic/Canary` (lowest priority)
225
+
226
+ **Common IANA Timezones:**
227
+ - Europe: `Atlantic/Canary`, `Europe/London`, `Europe/Paris`, `Europe/Berlin`
228
+ - Americas: `America/New_York`, `America/Chicago`, `America/Los_Angeles`, `America/Mexico_City`
229
+ - Asia: `Asia/Tokyo`, `Asia/Shanghai`, `Asia/Dubai`, `Asia/Kolkata`
230
+ - Other: `Atlantic/Canary`, `Pacific/Auckland`, `Australia/Sydney`
231
+
204
232
  ## Security
205
233
 
206
234
  - **Never** share your `client_secret` or `refresh_token`
207
235
  - Add `.env` to your `.gitignore` (already included)
208
- - Tokens have limited permissions only for Google Calendar
236
+ - Tokens have limited permissions only for Google Calendar and Google Tasks
209
237
 
210
238
  ## Additional Resources
211
239
 
212
240
  - [Google Calendar API Documentation](https://developers.google.com/calendar/api)
241
+ - [Google Tasks API Documentation](https://developers.google.com/tasks)
213
242
  - [OAuth 2.0 for Web Server Applications](https://developers.google.com/identity/protocols/oauth2/web-server)
214
243
  - [Google Cloud Console](https://console.cloud.google.com/)
215
244
  - [Model Context Protocol Documentation](https://modelcontextprotocol.io)
216
245
 
217
246
  ## Available Tools
218
247
 
219
- ### google-create-event
248
+ ### Calendar Tools
249
+
250
+ #### calendar-create-event
220
251
 
221
252
  Creates a new calendar event in Google Calendar.
222
253
 
@@ -226,12 +257,14 @@ Parameters:
226
257
  - `end`: DateTime string - Event end time (ISO 8601 format)
227
258
  - `description`: String (optional) - Event description
228
259
  - `location`: String (optional) - Event location
260
+ - `attendees`: Array of email strings (optional) - List of attendee email addresses
261
+ - `timeZone`: String (optional) - Timezone in IANA format (e.g., Atlantic/Canary, America/New_York). Defaults to `DEFAULT_TIMEZONE` from .env or 'Atlantic/Canary'
229
262
  - `calendarId`: String (optional) - Calendar ID (defaults to 'primary')
230
263
 
231
264
  Returns:
232
265
  - The unique ID and details of the created event
233
266
 
234
- ### google-update-event
267
+ #### calendar-update-event
235
268
 
236
269
  Updates an existing event in Google Calendar.
237
270
 
@@ -242,12 +275,14 @@ Parameters:
242
275
  - `end`: DateTime string (optional) - New event end time (ISO 8601 format)
243
276
  - `description`: String (optional) - New event description
244
277
  - `location`: String (optional) - New event location
278
+ - `attendees`: Array of email strings (optional) - List of attendee email addresses
279
+ - `timeZone`: String (optional) - Timezone in IANA format (e.g., Atlantic/Canary, America/New_York). Defaults to `DEFAULT_TIMEZONE` from .env or 'Atlantic/Canary'
245
280
  - `calendarId`: String (optional) - Calendar ID (defaults to 'primary')
246
281
 
247
282
  Returns:
248
283
  - The updated event details
249
284
 
250
- ### google-list-events
285
+ #### calendar-list-events
251
286
 
252
287
  Lists events within a specified timeframe from Google Calendar.
253
288
 
@@ -260,7 +295,7 @@ Parameters:
260
295
  Returns:
261
296
  - A list of events that fall within the given timeframe
262
297
 
263
- ### google-search-events
298
+ #### calendar-search-events
264
299
 
265
300
  Searches for events in Google Calendar by text query.
266
301
 
@@ -272,7 +307,7 @@ Parameters:
272
307
  Returns:
273
308
  - A list of events matching the search query
274
309
 
275
- ### google-delete-event
310
+ #### calendar-delete-event
276
311
 
277
312
  Deletes an event from Google Calendar.
278
313
 
@@ -283,23 +318,90 @@ Parameters:
283
318
  Returns:
284
319
  - Confirmation of deletion
285
320
 
286
- ### google-list-calendars
321
+ #### calendar-list-calendars
287
322
 
288
323
  Lists all calendars available to the user.
289
324
 
290
325
  Returns:
291
326
  - A list of calendars with their IDs and names
292
327
 
293
- ### google-get-current-datetime
328
+ #### calendar-get-current-datetime
294
329
 
295
330
  Gets the current date and time in ISO 8601 format. Useful for references when creating or searching for events.
296
331
 
297
332
  Parameters:
298
- - `timezone`: String (optional) - Timezone in IANA format (e.g., America/New_York, Europe/Madrid). Defaults to 'Atlantic/Canary'.
333
+ - `timezone`: String (optional) - Timezone in IANA format (e.g., Atlantic/Canary, America/New_York, Asia/Tokyo). Defaults to `DEFAULT_TIMEZONE` from .env or 'Atlantic/Canary'.
299
334
 
300
335
  Returns:
301
336
  - Current date and time information including ISO 8601 format, timestamp, local time, and timezone details
302
337
 
338
+ ### Tasks Tools
339
+
340
+ #### tasks-list-task-lists
341
+
342
+ Lists all task lists available in Google Tasks.
343
+
344
+ Parameters: None
345
+
346
+ Returns:
347
+ - A list of all task lists with their IDs and titles
348
+
349
+ Example use case:
350
+ - Get the list of all your task lists to find the `taskListId` for creating or managing tasks
351
+
352
+ #### tasks-list-tasks
353
+
354
+ Lists all tasks in a specific task list.
355
+
356
+ Parameters:
357
+ - `taskListId`: String - The ID of the task list (use `tasks-list-task-lists` to get this)
358
+ - `showCompleted`: Boolean (optional) - Whether to show completed tasks (default: true)
359
+ - `maxResults`: Number (optional) - Maximum number of tasks to return (default: 100)
360
+
361
+ Returns:
362
+ - A list of tasks with details including ID, title, notes, status, due date, and completion date
363
+
364
+ #### tasks-create-task
365
+
366
+ Creates a new task in Google Tasks.
367
+
368
+ Parameters:
369
+ - `taskListId`: String - The ID of the task list where the task will be created
370
+ - `title`: String - Task title/summary
371
+ - `notes`: String (optional) - Task notes or description
372
+ - `due`: DateTime string (optional) - Due date in ISO 8601 format (e.g., "2024-12-31T23:59:59Z")
373
+
374
+ Returns:
375
+ - The created task details including ID, title, notes, status, and due date
376
+
377
+
378
+ #### tasks-update-task
379
+
380
+ Updates an existing task in Google Tasks.
381
+
382
+ Parameters:
383
+ - `taskListId`: String - The ID of the task list containing the task
384
+ - `taskId`: String - The ID of the task to update
385
+ - `title`: String (optional) - New task title
386
+ - `notes`: String (optional) - New task notes or description
387
+ - `due`: DateTime string (optional) - New due date in ISO 8601 format
388
+ - `status`: String (optional) - Task status: either "needsAction" or "completed"
389
+
390
+ Returns:
391
+ - The updated task details
392
+
393
+
394
+ #### tasks-delete-task
395
+
396
+ Deletes a task from Google Tasks.
397
+
398
+ Parameters:
399
+ - `taskListId`: String - The ID of the task list containing the task
400
+ - `taskId`: String - The ID of the task to delete
401
+
402
+ Returns:
403
+ - Confirmation message of deletion
404
+
303
405
  ## License
304
406
 
305
407
  MIT
package/dist/index.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import "dotenv/config";
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
- import { GoogleCalendarClient } from "./tools/google-calendar.js";
5
+ import { GoogleCalendarClient } from "./tools/google-calendar-client.js";
6
6
  import { registerGoogleCalendarTools } from "./tools/google-calendar-mcp.js";
7
7
  const server = new McpServer({
8
8
  name: "mcp-google-calendar",
9
- version: "1.0.2",
9
+ version: "1.1.0",
10
10
  });
11
11
  async function main() {
12
12
  console.error('Configurando Google Calendar API...');
@@ -18,7 +18,6 @@ async function main() {
18
18
  const googleClient = new GoogleCalendarClient({
19
19
  clientId: process.env.GOOGLE_CLIENT_ID,
20
20
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
21
- redirectUrl: process.env.GOOGLE_REDIRECT_URL || 'http://localhost:8080/oauth/callback',
22
21
  refreshToken: process.env.GOOGLE_REFRESH_TOKEN,
23
22
  });
24
23
  // Si no tenemos refresh token, mostrar URL de autorización
@@ -1,16 +1,18 @@
1
- import { createSimpleEvent } from './google-calendar.js';
1
+ import { createSimpleEvent } from '../google-calendar-client.js';
2
2
  import { z } from 'zod';
3
3
  export function registerGoogleCreateEvent(googleClient, server) {
4
- server.tool('google_create_event', 'Crea un nuevo evento en Google Calendar', {
4
+ server.tool('calendar_create_event', 'Crea un nuevo evento en Google Calendar', {
5
5
  calendarId: z.string().optional().describe('ID del calendario (por defecto: primary)'),
6
6
  title: z.string().describe('Título del evento'),
7
7
  start: z.string().describe('Fecha/hora de inicio en formato ISO (ej: 2023-12-01T10:00:00Z)'),
8
8
  end: z.string().describe('Fecha/hora de fin en formato ISO'),
9
9
  description: z.string().optional().describe('Descripción del evento (opcional)'),
10
10
  location: z.string().optional().describe('Ubicación del evento (opcional)'),
11
+ attendees: z.array(z.string().email()).optional().describe('Lista de emails de los invitados (opcional)'),
12
+ timeZone: z.string().optional().describe('Zona horaria en formato IANA (ej: America/New_York, Atlantic/Canary). Por defecto usa DEFAULT_TIMEZONE del .env'),
11
13
  }, async (args) => {
12
14
  try {
13
- const { calendarId = 'primary', title, start, end, description, location, } = args;
15
+ const { calendarId = 'primary', title, start, end, description, location, attendees, timeZone, } = args;
14
16
  if (!title || !start || !end) {
15
17
  return {
16
18
  content: [
@@ -22,13 +24,17 @@ export function registerGoogleCreateEvent(googleClient, server) {
22
24
  isError: true,
23
25
  };
24
26
  }
25
- const event = createSimpleEvent(title, start, end, description, location);
27
+ const event = createSimpleEvent(title, start, end, description, location, attendees, timeZone);
26
28
  const createdEvent = await googleClient.createEvent(calendarId, event);
29
+ let successMessage = `Evento creado exitosamente:\nID: ${createdEvent.id}\nTítulo: ${createdEvent.summary}\nInicio: ${createdEvent.start?.dateTime}\nFin: ${createdEvent.end?.dateTime}`;
30
+ if (attendees && attendees.length > 0) {
31
+ successMessage += `\nInvitados: ${attendees.join(', ')}`;
32
+ }
27
33
  return {
28
34
  content: [
29
35
  {
30
36
  type: 'text',
31
- text: `Evento creado exitosamente:\nID: ${createdEvent.id}\nTítulo: ${createdEvent.summary}\nInicio: ${createdEvent.start?.dateTime}\nFin: ${createdEvent.end?.dateTime}`,
37
+ text: successMessage,
32
38
  },
33
39
  ],
34
40
  };
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  export function registerGoogleDeleteEvent(googleClient, server) {
3
- server.tool('google_delete_event', 'Elimina un evento de Google Calendar', {
3
+ server.tool('calendar_delete_event', 'Elimina un evento de Google Calendar', {
4
4
  calendarId: z.string().optional().describe('ID del calendario (por defecto: primary)'),
5
5
  eventId: z.string().describe('ID del evento a eliminar'),
6
6
  }, async (args) => {
@@ -1,10 +1,10 @@
1
1
  import { z } from 'zod';
2
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.'),
3
+ server.tool('calendar_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, Atlantic/Canary). Por defecto usa DEFAULT_TIMEZONE del .env'),
5
5
  }, async (args) => {
6
6
  try {
7
- const { timezone = 'Atlantic/Canary' } = args;
7
+ const { timezone = process.env.DEFAULT_TIMEZONE || 'Atlantic/Canary' } = args;
8
8
  const now = new Date();
9
9
  // Información básica
10
10
  const isoString = now.toISOString();
@@ -40,7 +40,7 @@ export function registerGoogleGetCurrentDatetime(googleClient, server) {
40
40
  content: [
41
41
  {
42
42
  type: 'text',
43
- text: `Error: Zona horaria inválida '${timezone}'. Usa formato IANA (ej: America/New_York, Europe/Madrid, Atlantic/Canary)`,
43
+ text: `Error: Zona horaria inválida '${timezone}'. Usa formato IANA (ej: America/New_York, Atlantic/Canary, Atlantic/Canary)`,
44
44
  },
45
45
  ],
46
46
  isError: true,
@@ -1,5 +1,5 @@
1
1
  export function registerGoogleListCalendars(googleClient, server) {
2
- server.tool('google_list_calendars', 'Lista todos los calendarios disponibles en Google Calendar', async () => {
2
+ server.tool('calendar_list_calendars', 'Lista todos los calendarios disponibles en Google Calendar', async () => {
3
3
  try {
4
4
  const calendars = await googleClient.listCalendars();
5
5
  const data = calendars.map((cal) => ({
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  export function registerGoogleListEvents(googleClient, server) {
3
- server.tool('google_list_events', 'Lista eventos de un calendario de Google Calendar', {
3
+ server.tool('calendar_list_events', 'Lista eventos de un calendario de Google Calendar', {
4
4
  calendarId: z.string().optional().describe('ID del calendario (por defecto: primary)'),
5
5
  timeMin: z.string().optional().describe('Fecha/hora mínima en formato ISO (ej: 2023-12-01T00:00:00Z)'),
6
6
  timeMax: z.string().optional().describe('Fecha/hora máxima en formato ISO'),
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  export function registerGoogleSearchEvents(googleClient, server) {
3
- server.tool('google_search_events', 'Busca eventos en Google Calendar por texto', {
3
+ server.tool('calendar_search_events', 'Busca eventos en Google Calendar por texto', {
4
4
  calendarId: z.string().optional().describe('ID del calendario (por defecto: primary)'),
5
5
  query: z.string().describe('Texto a buscar en los eventos'),
6
6
  maxResults: z.number().optional().describe('Número máximo de resultados (por defecto: 10)'),
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  export function registerGoogleUpdateEvent(googleClient, server) {
3
- server.tool('google_update_event', 'Actualiza un evento existente en Google Calendar', {
3
+ server.tool('calendar_update_event', 'Actualiza un evento existente en Google Calendar', {
4
4
  calendarId: z.string().optional().describe('ID del calendario (por defecto: primary)'),
5
5
  eventId: z.string().describe('ID del evento a actualizar'),
6
6
  title: z.string().optional().describe('Nuevo título del evento'),
@@ -8,9 +8,11 @@ export function registerGoogleUpdateEvent(googleClient, server) {
8
8
  end: z.string().optional().describe('Nueva fecha/hora de fin en formato ISO'),
9
9
  description: z.string().optional().describe('Nueva descripción del evento'),
10
10
  location: z.string().optional().describe('Nueva ubicación del evento'),
11
+ attendees: z.array(z.string().email()).optional().describe('Lista de emails de los invitados (opcional)'),
12
+ timeZone: z.string().optional().describe('Zona horaria en formato IANA (ej: America/New_York, Atlantic/Canary). Por defecto usa DEFAULT_TIMEZONE del .env'),
11
13
  }, async (args) => {
12
14
  try {
13
- const { calendarId = 'primary', eventId, title, start, end, description, location, } = args;
15
+ const { calendarId = 'primary', eventId, title, start, end, description, location, attendees, timeZone, } = args;
14
16
  if (!eventId) {
15
17
  return {
16
18
  content: [
@@ -33,24 +35,35 @@ export function registerGoogleUpdateEvent(googleClient, server) {
33
35
  if (location !== undefined) {
34
36
  event.location = location;
35
37
  }
38
+ const defaultTimeZone = timeZone || process.env.DEFAULT_TIMEZONE || 'Atlantic/Canary';
36
39
  if (start !== undefined) {
37
40
  event.start = {
38
41
  dateTime: start,
39
- timeZone: 'America/Mexico_City',
42
+ timeZone: defaultTimeZone,
40
43
  };
41
44
  }
42
45
  if (end !== undefined) {
43
46
  event.end = {
44
47
  dateTime: end,
45
- timeZone: 'America/Mexico_City',
48
+ timeZone: defaultTimeZone,
46
49
  };
47
50
  }
51
+ if (attendees !== undefined) {
52
+ event.attendees = attendees.map(email => ({
53
+ email: email.trim(),
54
+ responseStatus: 'needsAction'
55
+ }));
56
+ }
48
57
  const updatedEvent = await googleClient.updateEvent(calendarId, eventId, event);
58
+ let successMessage = `Evento actualizado exitosamente:\nID: ${updatedEvent.id}\nTítulo: ${updatedEvent.summary}\nInicio: ${updatedEvent.start?.dateTime}\nFin: ${updatedEvent.end?.dateTime}`;
59
+ if (attendees !== undefined && attendees.length > 0) {
60
+ successMessage += `\nInvitados: ${attendees.join(', ')}`;
61
+ }
49
62
  return {
50
63
  content: [
51
64
  {
52
65
  type: 'text',
53
- text: `Evento actualizado exitosamente:\nID: ${updatedEvent.id}\nTítulo: ${updatedEvent.summary}\nInicio: ${updatedEvent.start?.dateTime}\nFin: ${updatedEvent.end?.dateTime}`,
66
+ text: successMessage,
54
67
  },
55
68
  ],
56
69
  };
@@ -2,14 +2,16 @@ import { google } from 'googleapis';
2
2
  export class GoogleCalendarClient {
3
3
  auth;
4
4
  calendar;
5
+ tasks;
5
6
  constructor(config) {
6
- this.auth = new google.auth.OAuth2(config.clientId, config.clientSecret, config.redirectUrl);
7
+ this.auth = new google.auth.OAuth2(config.clientId, config.clientSecret, 'http://localhost:8080/oauth/callback');
7
8
  if (config.refreshToken) {
8
9
  this.auth.setCredentials({
9
10
  refresh_token: config.refreshToken,
10
11
  });
11
12
  }
12
13
  this.calendar = google.calendar({ version: 'v3', auth: this.auth });
14
+ this.tasks = google.tasks({ version: 'v1', auth: this.auth });
13
15
  }
14
16
  /**
15
17
  * Genera URL de autorización para obtener el token inicial
@@ -17,7 +19,8 @@ export class GoogleCalendarClient {
17
19
  getAuthUrl() {
18
20
  const scopes = [
19
21
  'https://www.googleapis.com/auth/calendar',
20
- 'https://www.googleapis.com/auth/calendar.events'
22
+ 'https://www.googleapis.com/auth/calendar.events',
23
+ 'https://www.googleapis.com/auth/tasks'
21
24
  ];
22
25
  return this.auth.generateAuthUrl({
23
26
  access_type: 'offline',
@@ -132,22 +135,129 @@ export class GoogleCalendarClient {
132
135
  throw error;
133
136
  }
134
137
  }
138
+ // ========== Google Tasks Methods ==========
139
+ /**
140
+ * Lista todas las listas de tareas del usuario
141
+ */
142
+ async listTaskLists() {
143
+ try {
144
+ const response = await this.tasks.tasklists.list();
145
+ return response.data.items || [];
146
+ }
147
+ catch (error) {
148
+ console.error('Error al listar listas de tareas:', error);
149
+ throw error;
150
+ }
151
+ }
152
+ /**
153
+ * Lista todas las tareas de una lista específica
154
+ */
155
+ async listTasks(taskListId, showCompleted = true, maxResults = 100) {
156
+ try {
157
+ const response = await this.tasks.tasks.list({
158
+ tasklist: taskListId,
159
+ showCompleted,
160
+ maxResults,
161
+ });
162
+ return response.data.items || [];
163
+ }
164
+ catch (error) {
165
+ console.error('Error al listar tareas:', error);
166
+ throw error;
167
+ }
168
+ }
169
+ /**
170
+ * Crea una nueva tarea
171
+ */
172
+ async createTask(taskListId, task) {
173
+ try {
174
+ const response = await this.tasks.tasks.insert({
175
+ tasklist: taskListId,
176
+ requestBody: task,
177
+ });
178
+ return response.data;
179
+ }
180
+ catch (error) {
181
+ console.error('Error al crear tarea:', error);
182
+ throw error;
183
+ }
184
+ }
185
+ /**
186
+ * Actualiza una tarea existente
187
+ */
188
+ async updateTask(taskListId, taskId, task) {
189
+ try {
190
+ const response = await this.tasks.tasks.update({
191
+ tasklist: taskListId,
192
+ task: taskId,
193
+ requestBody: task,
194
+ });
195
+ return response.data;
196
+ }
197
+ catch (error) {
198
+ console.error('Error al actualizar tarea:', error);
199
+ throw error;
200
+ }
201
+ }
202
+ /**
203
+ * Elimina una tarea
204
+ */
205
+ async deleteTask(taskListId, taskId) {
206
+ try {
207
+ await this.tasks.tasks.delete({
208
+ tasklist: taskListId,
209
+ task: taskId,
210
+ });
211
+ }
212
+ catch (error) {
213
+ console.error('Error al eliminar tarea:', error);
214
+ throw error;
215
+ }
216
+ }
217
+ /**
218
+ * Marca una tarea como completada
219
+ */
220
+ async completeTask(taskListId, taskId) {
221
+ try {
222
+ // Primero obtenemos la tarea para no perder información
223
+ const taskResponse = await this.tasks.tasks.get({
224
+ tasklist: taskListId,
225
+ task: taskId,
226
+ });
227
+ const task = taskResponse.data;
228
+ task.status = 'completed';
229
+ task.completed = new Date().toISOString();
230
+ return await this.updateTask(taskListId, taskId, task);
231
+ }
232
+ catch (error) {
233
+ console.error('Error al completar tarea:', error);
234
+ throw error;
235
+ }
236
+ }
135
237
  }
136
238
  /**
137
239
  * Función helper para crear eventos con formato simplificado
138
240
  */
139
- export function createSimpleEvent(title, start, end, description, location) {
140
- return {
241
+ export function createSimpleEvent(title, start, end, description, location, attendees, timeZone) {
242
+ const defaultTimeZone = timeZone || process.env.DEFAULT_TIMEZONE || 'Atlantic/Canary';
243
+ const event = {
141
244
  summary: title,
142
245
  description,
143
246
  location,
144
247
  start: {
145
248
  dateTime: start,
146
- timeZone: 'America/Mexico_City', // Ajusta según tu zona horaria
249
+ timeZone: defaultTimeZone,
147
250
  },
148
251
  end: {
149
252
  dateTime: end,
150
- timeZone: 'America/Mexico_City',
253
+ timeZone: defaultTimeZone,
151
254
  },
152
255
  };
256
+ if (attendees && attendees.length > 0) {
257
+ event.attendees = attendees.map(email => ({
258
+ email: email.trim(),
259
+ responseStatus: 'needsAction'
260
+ }));
261
+ }
262
+ return event;
153
263
  }
@@ -1,11 +1,17 @@
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';
1
+ import { registerGoogleListCalendars } from './calendar/calendar-list-calendars.js';
2
+ import { registerGoogleListEvents } from './calendar/calendar-list-events.js';
3
+ import { registerGoogleCreateEvent } from './calendar/calendar-create-event.js';
4
+ import { registerGoogleUpdateEvent } from './calendar/calendar-update-event.js';
5
+ import { registerGoogleDeleteEvent } from './calendar/calendar-delete-event.js';
6
+ import { registerGoogleSearchEvents } from './calendar/calendar-search-events.js';
7
+ import { registerGoogleGetCurrentDatetime } from './calendar/calendar-get-current-datetime.js';
8
+ import { registerTasksListTaskLists } from './tasks/tasks-list-task-lists.js';
9
+ import { registerTasksListTasks } from './tasks/tasks-list-tasks.js';
10
+ import { registerTasksCreateTask } from './tasks/tasks-create-task.js';
11
+ import { registerTasksUpdateTask } from './tasks/tasks-update-task.js';
12
+ import { registerTasksDeleteTask } from './tasks/tasks-delete-task.js';
8
13
  export function registerGoogleCalendarTools(googleClient, server) {
14
+ // Calendar tools
9
15
  registerGoogleListCalendars(googleClient, server);
10
16
  registerGoogleListEvents(googleClient, server);
11
17
  registerGoogleCreateEvent(googleClient, server);
@@ -13,4 +19,10 @@ export function registerGoogleCalendarTools(googleClient, server) {
13
19
  registerGoogleDeleteEvent(googleClient, server);
14
20
  registerGoogleSearchEvents(googleClient, server);
15
21
  registerGoogleGetCurrentDatetime(googleClient, server);
22
+ // Tasks tools
23
+ registerTasksListTaskLists(googleClient, server);
24
+ registerTasksListTasks(googleClient, server);
25
+ registerTasksCreateTask(googleClient, server);
26
+ registerTasksUpdateTask(googleClient, server);
27
+ registerTasksDeleteTask(googleClient, server);
16
28
  }
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+ export function registerTasksCreateTask(googleClient, server) {
3
+ server.tool('tasks_create_task', 'Crea una nueva tarea en Google Tasks', {
4
+ taskListId: z.string().describe('ID de la lista de tareas (usa tasks_list_task_lists para obtenerlo)'),
5
+ title: z.string().describe('Título de la tarea'),
6
+ notes: z.string().optional().describe('Notas o descripción de la tarea'),
7
+ due: z.string().optional().describe('Fecha de vencimiento en formato ISO (ej: 2023-12-31T23:59:59Z)'),
8
+ }, async (args) => {
9
+ try {
10
+ const { taskListId, title, notes, due, } = args;
11
+ const task = {
12
+ title,
13
+ notes,
14
+ due,
15
+ status: 'needsAction',
16
+ };
17
+ const createdTask = await googleClient.createTask(taskListId, task);
18
+ return {
19
+ content: [
20
+ {
21
+ type: 'text',
22
+ text: JSON.stringify({
23
+ id: createdTask.id,
24
+ title: createdTask.title,
25
+ notes: createdTask.notes,
26
+ status: createdTask.status,
27
+ due: createdTask.due,
28
+ created: createdTask.updated,
29
+ }, null, 2),
30
+ },
31
+ ],
32
+ };
33
+ }
34
+ catch (error) {
35
+ const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
36
+ return {
37
+ content: [
38
+ {
39
+ type: 'text',
40
+ text: `Error al crear tarea: ${errorMessage}`,
41
+ },
42
+ ],
43
+ isError: true,
44
+ };
45
+ }
46
+ });
47
+ }
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ export function registerTasksDeleteTask(googleClient, server) {
3
+ server.tool('tasks_delete_task', 'Elimina una tarea de Google Tasks', {
4
+ taskListId: z.string().describe('ID de la lista de tareas'),
5
+ taskId: z.string().describe('ID de la tarea a eliminar'),
6
+ }, async (args) => {
7
+ try {
8
+ const { taskListId, taskId } = args;
9
+ await googleClient.deleteTask(taskListId, taskId);
10
+ return {
11
+ content: [
12
+ {
13
+ type: 'text',
14
+ text: `Tarea ${taskId} eliminada exitosamente de la lista ${taskListId}`,
15
+ },
16
+ ],
17
+ };
18
+ }
19
+ catch (error) {
20
+ const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
21
+ return {
22
+ content: [
23
+ {
24
+ type: 'text',
25
+ text: `Error al eliminar tarea: ${errorMessage}`,
26
+ },
27
+ ],
28
+ isError: true,
29
+ };
30
+ }
31
+ });
32
+ }
@@ -0,0 +1,32 @@
1
+ export function registerTasksListTaskLists(googleClient, server) {
2
+ server.tool('tasks_list_task_lists', 'Lista todas las listas de tareas de Google Tasks del usuario', {}, async () => {
3
+ try {
4
+ const taskLists = await googleClient.listTaskLists();
5
+ const data = taskLists.map((taskList) => ({
6
+ id: taskList.id,
7
+ title: taskList.title,
8
+ updated: taskList.updated,
9
+ }));
10
+ return {
11
+ content: [
12
+ {
13
+ type: 'text',
14
+ text: JSON.stringify(data, null, 2),
15
+ },
16
+ ],
17
+ };
18
+ }
19
+ catch (error) {
20
+ const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
21
+ return {
22
+ content: [
23
+ {
24
+ type: 'text',
25
+ text: `Error al listar listas de tareas: ${errorMessage}`,
26
+ },
27
+ ],
28
+ isError: true,
29
+ };
30
+ }
31
+ });
32
+ }
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ export function registerTasksListTasks(googleClient, server) {
3
+ server.tool('tasks_list_tasks', 'Lista todas las tareas de una lista de tareas específica en Google Tasks', {
4
+ taskListId: z.string().describe('ID de la lista de tareas (usa tasks_list_task_lists para obtenerlo)'),
5
+ showCompleted: z.boolean().optional().describe('Mostrar tareas completadas (por defecto: true)'),
6
+ maxResults: z.number().optional().describe('Número máximo de resultados (por defecto: 100)'),
7
+ }, async (args) => {
8
+ try {
9
+ const { taskListId, showCompleted = true, maxResults = 100, } = args;
10
+ const tasks = await googleClient.listTasks(taskListId, showCompleted, maxResults);
11
+ const data = tasks.map((task) => ({
12
+ id: task.id,
13
+ title: task.title,
14
+ notes: task.notes,
15
+ status: task.status,
16
+ due: task.due,
17
+ completed: task.completed,
18
+ updated: task.updated,
19
+ parent: task.parent,
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 tareas: ${errorMessage}`,
37
+ },
38
+ ],
39
+ isError: true,
40
+ };
41
+ }
42
+ });
43
+ }
@@ -0,0 +1,57 @@
1
+ import { z } from 'zod';
2
+ export function registerTasksUpdateTask(googleClient, server) {
3
+ server.tool('tasks_update_task', 'Actualiza una tarea existente en Google Tasks', {
4
+ taskListId: z.string().describe('ID de la lista de tareas'),
5
+ taskId: z.string().describe('ID de la tarea a actualizar'),
6
+ title: z.string().optional().describe('Nuevo título de la tarea'),
7
+ notes: z.string().optional().describe('Nuevas notas o descripción de la tarea'),
8
+ due: z.string().optional().describe('Nueva fecha de vencimiento en formato ISO (ej: 2023-12-31T23:59:59Z)'),
9
+ status: z.enum(['needsAction', 'completed']).optional().describe('Estado de la tarea'),
10
+ }, async (args) => {
11
+ try {
12
+ const { taskListId, taskId, title, notes, due, status, } = args;
13
+ const task = {};
14
+ if (title !== undefined)
15
+ task.title = title;
16
+ if (notes !== undefined)
17
+ task.notes = notes;
18
+ if (due !== undefined)
19
+ task.due = due;
20
+ if (status !== undefined) {
21
+ task.status = status;
22
+ if (status === 'completed') {
23
+ task.completed = new Date().toISOString();
24
+ }
25
+ }
26
+ const updatedTask = await googleClient.updateTask(taskListId, taskId, task);
27
+ return {
28
+ content: [
29
+ {
30
+ type: 'text',
31
+ text: JSON.stringify({
32
+ id: updatedTask.id,
33
+ title: updatedTask.title,
34
+ notes: updatedTask.notes,
35
+ status: updatedTask.status,
36
+ due: updatedTask.due,
37
+ completed: updatedTask.completed,
38
+ updated: updatedTask.updated,
39
+ }, null, 2),
40
+ },
41
+ ],
42
+ };
43
+ }
44
+ catch (error) {
45
+ const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
46
+ return {
47
+ content: [
48
+ {
49
+ type: 'text',
50
+ text: `Error al actualizar tarea: ${errorMessage}`,
51
+ },
52
+ ],
53
+ isError: true,
54
+ };
55
+ }
56
+ });
57
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pablosr/mcp-google-calendar",
3
3
  "description": "A Google Calendar (MCP) server to expose calendar operations as tools for LLM.",
4
- "version": "1.0.2",
4
+ "version": "1.1.0",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
7
7
  "bin": "dist/index.js",