#
tokens: 5674/50000 7/7 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── devbox.json
├── devbox.lock
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── index.ts
│   └── types.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
node_modules/
build/
*.log
.env*
.client_secret.json
.gcal-tokens-*.json
.gcal-settings.json
.devbox

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Google Calendar MCP Server

Interact with your Google Calendar through Claude Desktop using the Model Context Protocol (MCP).

This is a TypeScript-based MCP server that implements Google Calendar integration. It demonstrates core MCP concepts while providing:

- Calendar event management through MCP URIs
- Tools for creating and modifying events
- Prompts for generating calendar insights

## Features

### Resources
- Access calendar events via MCP URIs
- Each event has title, time, description, and attendees
- Structured event data with proper mime types

### Tools
- `create_event` - Create new calendar events
  - Takes title, time, and other event details as parameters
  - Directly interfaces with Google Calendar API
- `list_events` - View upcoming calendar events
- [Add other tools you've implemented]

### Prompts
- `analyze_schedule` - Generate insights about your calendar
  - Includes upcoming events as embedded resources
  - Returns structured prompt for LLM analysis
- [Add other prompts you've implemented]

## Prerequisites

- Node.js (v14 or higher)
- A Google Cloud Project with Calendar API enabled
- OAuth 2.0 Client credentials

## Development

Install devbox by following instructions at [devbox.sh](https://www.jetpack.io/devbox)
```bash
curl -fsSL https://get.jetpack.io/devbox | bash
```

Initialize devbox in the project directory:
```bash
devbox init
```

Start the devbox shell:
```bash
devbox shell
```

Install dependencies:
```bash
npm install
```

Build the server:
```bash
npm run build
```

For development with auto-rebuild:
```bash
npm run watch
```

## Installation

To use with Claude Desktop, add the server config:

On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
On Windows: `%APPDATA%/Claude/claude_desktop_config.json`

```json
{
  "mcpServers": {
    "Google Calendar": {
      "command": "/path/to/Google Calendar/build/index.js"
    }
  }
}
```

## First-Time Setup

1. Set up Google Cloud credentials:
   - Go to [Google Cloud Console](https://console.cloud.google.com)
   - Create a new project or select an existing one
   - Enable the Google Calendar API
   - Create OAuth 2.0 credentials (Desktop application type)
   - Download the client secret JSON file
   - Rename it to `.client_secret.json` and place it in the project root

2. Initial Authentication:
   - When first running the server, it will provide an authentication URL
   - Visit the URL in your browser
   - Grant the requested permissions
   - Copy the provided authorization code
   - Paste the code back into the CLI prompt

### Debugging

Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script:

```bash
npm run inspector
```

The Inspector will provide a URL to access debugging tools in your browser.

```

--------------------------------------------------------------------------------
/devbox.json:
--------------------------------------------------------------------------------

```json
{
  "$schema":  "https://raw.githubusercontent.com/jetify-com/devbox/0.13.6/.schema/devbox.schema.json",
  "packages": ["nodejs@latest"],
  "shell": {
    "init_hook": [
      "echo 'Welcome to devbox!' > /dev/null"
    ],
    "scripts": {
      "test": [
        "echo \"Error: no test specified\" && exit 1"
      ]
    }
  }
}

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "gcal-mcp-server",
  "version": "0.1.0",
  "description": "Interact with your google calendar",
  "private": true,
  "type": "module",
  "bin": {
    "Google Calendar": "./build/index.js"
  },
  "files": [
    "build"
  ],
  "scripts": {
    "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
    "prepare": "npm run build",
    "watch": "tsc --watch",
    "inspector": "npx @modelcontextprotocol/inspector build/index.js",
    "auth": "npm run build && node build/index.js auth"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.6.1",
    "google-auth-library": "^9.15.0",
    "googleapis": "^144.0.0",
    "zod": "^3.23.8",
    "zod-to-json-schema": "^3.23.5"
  },
  "devDependencies": {
    "@types/node": "^20.11.24",
    "typescript": "^5.3.3"
  }
}

```

--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------

```typescript
// ... existing imports ...
import { z } from "zod";

// Add schemas at the top of the file after imports
export const CreateEventArgsSchema = z.object({
  accountId: z.string(),
  calendarId: z.string().optional(),
  summary: z.string(),
  description: z.string().optional(),
  location: z.string().optional(),
  start: z.string(),
  end: z.string()
});

export const SearchEventsArgsSchema = z.object({
  accountId: z.string(),
  query: z.string()
});

export const ListEventsArgsSchema = z.object({
  accountId: z.string(),
  calendarId: z.string().optional(),
  maxResults: z.number().optional().default(10),
  timeMin: z.string().optional(),
  timeMax: z.string().optional()
});

export const SetCalendarDefaultsArgsSchema = z.object({
  accountId: z.string(),
  calendarId: z.string().optional()
});

export const ListCalendarsArgsSchema = z.object({
  accountId: z.string()
});

export type CreateEventArgs = z.infer<typeof CreateEventArgsSchema>;
export type SearchEventsArgs = z.infer<typeof SearchEventsArgsSchema>;
export type ListEventsArgs = z.infer<typeof ListEventsArgsSchema>;
export type SetCalendarDefaultsArgs = z.infer<typeof SetCalendarDefaultsArgsSchema>;
export type ListCalendarsArgs = z.infer<typeof ListCalendarsArgsSchema>;

// ... existing code ...

// Then in the CallToolRequestSchema handler, update the cases:

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/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);
  });
}

```