# Directory Structure ``` ├── .env ├── .gitignore ├── docs │ └── Demo-on-Dive-Desktop.png ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── calendar-tools │ │ ├── _index.ts │ │ ├── calendarTools.ts │ │ ├── createEvent.ts │ │ ├── deleteEvent.ts │ │ ├── listEvents.ts │ │ └── updateEvent.ts │ ├── index.ts │ └── utils │ ├── auth.ts │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- ``` 1 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | dist/ 2 | node_modules/ 3 | credentials.json 4 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Calendar Tools MCP Server 2 | 3 | A powerful Model Context Protocol (MCP) server providing comprehensive calendar management capabilities. 4 | 5 | ## Features 6 | 7 | ### Calendar Management 8 | 9 | - Create calendar events 10 | - List calendar events 11 | - Update existing events 12 | - Delete events 13 | 14 | ## Demo on Dive Desktop 15 | 16 |  17 | 18 | ## Installation 19 | 20 | ### Manual Installation 21 | 22 | ```bash 23 | npm install -g @cablate/mcp-google-calendar 24 | ``` 25 | 26 | ## Usage 27 | 28 | ### Cli 29 | 30 | ```bash 31 | mcp-google-calendar 32 | ``` 33 | 34 | ### With [Dive Desktop](https://github.com/OpenAgentPlatform/Dive) 35 | 36 | 1. Click "+ Add MCP Server" in Dive Desktop 37 | 2. Copy and paste this configuration: 38 | 39 | ```json 40 | { 41 | "mcpServers": { 42 | "calendar": { 43 | "command": "npx", 44 | "args": ["-y", "@cablate/mcp-google-calendar"], 45 | "env": { 46 | "GOOGLE_CALENDAR_ID": "your_calendar_id", 47 | "GOOGLE_TIME_ZONE": "your_time_zone", 48 | "GOOGLE_CREDENTIALS_PATH": "your_credentials_path" 49 | }, 50 | "enabled": true 51 | } 52 | } 53 | } 54 | ``` 55 | 56 | 3. Click "Save" to install the MCP server 57 | 58 | ## Google Service Account and Credentials 59 | 60 | Here is the simple steps to create a google service account and credentials: 61 | 62 | 1. Go to [Google Cloud Console](https://console.cloud.google.com/) 63 | 2. Create a new project or select an existing project 64 | 3. Navigate to the "IAM & Admin" section 65 | 4. Click on "Service Accounts" 66 | 5. Click on "Create Service Account" 67 | 6. Enter a name for the service account (e.g., "MCP Google Calendar") 68 | 7. Click on "Create" 69 | 8. Click on "Create Key" 70 | 9. Select "JSON" as the key type 71 | 10. Click on "Create" 72 | 11. Download the JSON file and save it as `credentials.json` 73 | 74 | if still got any question, google and find the answer. 75 | 76 | ## License 77 | 78 | MIT 79 | 80 | ## Contributing 81 | 82 | Welcome community participation and contributions! Here are ways to contribute: 83 | 84 | - ⭐️ Star the project if you find it helpful 85 | - 🐛 Submit Issues: Report problems or provide suggestions 86 | - 🔧 Create Pull Requests: Submit code improvements 87 | 88 | ## Contact 89 | 90 | If you have any questions or suggestions, feel free to reach out: 91 | 92 | - 📧 Email: [[email protected]](mailto:[email protected]) 93 | - 📧 GitHub: [CabLate](https://github.com/cablate/) 94 | - 🤝 Collaboration: Welcome to discuss project cooperation 95 | - 📚 Technical Guidance: Sincere welcome for suggestions and guidance 96 | ``` -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export function convertToTimeZone(dateTime: string, timeZone: string): string { 2 | return new Date( 3 | new Date(dateTime).toLocaleString('en-US', { 4 | timeZone: timeZone 5 | }) 6 | ).toISOString(); 7 | } ``` -------------------------------------------------------------------------------- /src/calendar-tools/_index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CREATE_EVENT_TOOL, DELETE_EVENT_TOOL, LIST_EVENTS_TOOL, UPDATE_EVENT_TOOL } from "./calendarTools.js"; 2 | 3 | 4 | export const tools = [CREATE_EVENT_TOOL, LIST_EVENTS_TOOL, UPDATE_EVENT_TOOL, DELETE_EVENT_TOOL]; 5 | 6 | export * from "./calendarTools.js"; 7 | export * from "./createEvent.js"; 8 | export * from "./deleteEvent.js"; 9 | export * from "./listEvents.js"; 10 | export * from "./updateEvent.js"; 11 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "strict": true, 5 | "esModuleInterop": true, 6 | "skipLibCheck": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "outDir": "./dist", 10 | "rootDir": "./src", 11 | "moduleResolution": "NodeNext", 12 | "module": "NodeNext", 13 | "noImplicitAny": false 14 | }, 15 | "exclude": ["node_modules"], 16 | "include": ["src/**/*"] 17 | } ``` -------------------------------------------------------------------------------- /src/calendar-tools/deleteEvent.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { google } from "googleapis"; 2 | import { getAuthClient } from "../utils/auth.js"; 3 | 4 | export async function deleteEvent(eventId: string) { 5 | try { 6 | const auth = await getAuthClient(); 7 | const calendar = google.calendar({ version: "v3", auth }); 8 | 9 | await calendar.events.delete({ 10 | calendarId: process.env.GOOGLE_CALENDAR_ID || "primary", 11 | eventId: eventId, 12 | sendUpdates: "all", 13 | }); 14 | 15 | return { 16 | success: true, 17 | data: "Event successfully deleted", 18 | }; 19 | } catch (error) { 20 | return { 21 | success: false, 22 | error: error instanceof Error ? error.message : "Error deleting event", 23 | }; 24 | } 25 | } 26 | ``` -------------------------------------------------------------------------------- /src/utils/auth.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { promises as fs } from "fs"; 2 | import { JWT } from "google-auth-library"; 3 | import path from "path"; 4 | 5 | const SCOPES = ["https://www.googleapis.com/auth/calendar"]; 6 | const CREDENTIALS_PATH = process.env.GOOGLE_CREDENTIALS_PATH || path.join(process.cwd(), "credentials.json"); 7 | 8 | export async function getAuthClient(): Promise<JWT> { 9 | try { 10 | const content = await fs.readFile(CREDENTIALS_PATH); 11 | const credentials = JSON.parse(content.toString()); 12 | 13 | const client = new JWT({ 14 | email: credentials.client_email, 15 | key: credentials.private_key, 16 | scopes: SCOPES, 17 | }); 18 | 19 | return client; 20 | } catch (error) { 21 | console.error("Authentication error:", error); 22 | throw error; 23 | } 24 | } 25 | ``` -------------------------------------------------------------------------------- /src/calendar-tools/listEvents.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { calendar_v3, google } from "googleapis"; 2 | import { getAuthClient } from "../utils/auth.js"; 3 | import { convertToTimeZone } from "../utils/index.js"; 4 | 5 | interface ListEventsParams { 6 | timeMin: string; 7 | maxResults: number; 8 | } 9 | 10 | interface ListEventsResult { 11 | success: boolean; 12 | error?: string; 13 | data?: calendar_v3.Schema$Event[]; 14 | } 15 | 16 | export async function listEvents(params: ListEventsParams): Promise<ListEventsResult> { 17 | try { 18 | const auth = await getAuthClient(); 19 | const calendar = google.calendar({ version: "v3", auth }); 20 | 21 | const result = await calendar.events.list({ 22 | calendarId: process.env.GOOGLE_CALENDAR_ID || "primary", 23 | timeMin: convertToTimeZone(params.timeMin, process.env.GOOGLE_TIME_ZONE || "Asia/Taipei"), 24 | maxResults: params.maxResults, 25 | singleEvents: true, 26 | orderBy: "startTime", 27 | }); 28 | 29 | return { 30 | success: true, 31 | data: result.data?.items || [], 32 | }; 33 | } catch (error) { 34 | return { 35 | success: false, 36 | error: error instanceof Error ? error.message : "Error getting event list", 37 | }; 38 | } 39 | } 40 | ``` -------------------------------------------------------------------------------- /src/calendar-tools/createEvent.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { calendar_v3, google } from "googleapis"; 2 | import { getAuthClient } from "../utils/auth.js"; 3 | 4 | interface CreateEventParams { 5 | summary: string; 6 | description?: string; 7 | startTime: string; 8 | endTime: string; 9 | attendees?: string[]; 10 | } 11 | 12 | interface CreateEventResult { 13 | success: boolean; 14 | error?: string; 15 | data?: calendar_v3.Schema$Event; 16 | } 17 | 18 | export async function createEvent(params: CreateEventParams): Promise<CreateEventResult> { 19 | try { 20 | const auth = await getAuthClient(); 21 | const calendar = google.calendar({ version: "v3", auth }); 22 | 23 | const event = { 24 | summary: params.summary, 25 | description: params.description, 26 | start: { 27 | dateTime: params.startTime, 28 | timeZone: process.env.GOOGLE_TIME_ZONE || "Etc/UTC", 29 | }, 30 | end: { 31 | dateTime: params.endTime, 32 | timeZone: process.env.GOOGLE_TIME_ZONE || "Etc/UTC", 33 | }, 34 | attendees: params.attendees?.map((email) => ({ email })), 35 | }; 36 | 37 | const result = await calendar.events.insert({ 38 | calendarId: process.env.GOOGLE_CALENDAR_ID || "primary", 39 | requestBody: event, 40 | sendUpdates: "all", 41 | }); 42 | 43 | return { 44 | success: true, 45 | data: result.data || undefined, 46 | }; 47 | } catch (error) { 48 | return { 49 | success: false, 50 | error: error instanceof Error ? error.message : "Error creating event", 51 | }; 52 | } 53 | } 54 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@cablate/mcp-google-calendar", 3 | "version": "0.0.3", 4 | "type": "module", 5 | "description": "MCP server that provides google calendar capabilities", 6 | "main": "dist/index.cjs", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.cjs --external:pdfreader --external:jsdom --external:mammoth --external:csv-parse --external:libreoffice-convert && shx chmod +x dist/index.cjs", 10 | "dev": "ts-node src/index.ts", 11 | "start": "node dist/index.cjs" 12 | }, 13 | "dependencies": { 14 | "@google-cloud/local-auth": "^2.1.0", 15 | "@modelcontextprotocol/sdk": "^1.5.0", 16 | "esbuild": "^0.25.0", 17 | "googleapis": "^105.0.0", 18 | "shx": "^0.3.4", 19 | "typescript": "^5.3.3" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^20.11.16", 23 | "ts-node": "^10.9.2" 24 | }, 25 | "author": "CabLate", 26 | "files": [ 27 | "dist", 28 | "dist/**/*.map", 29 | "README.md" 30 | ], 31 | "bin": { 32 | "mcp-doc-forge": "./dist/index.cjs" 33 | }, 34 | "keywords": [ 35 | "mcp", 36 | "mcp-server", 37 | "google-calendar", 38 | "google-api", 39 | "calendar", 40 | "ai", 41 | "dive" 42 | ], 43 | "homepage": "https://github.com/cablate/mcp-google-calendar#readme", 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/cablate/mcp-google-calendar.git" 47 | }, 48 | "bugs": { 49 | "url": "https://github.com/cablate/mcp-google-calendar/issues" 50 | } 51 | } 52 | ``` -------------------------------------------------------------------------------- /src/calendar-tools/updateEvent.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { google } from "googleapis"; 2 | import { getAuthClient } from "../utils/auth.js"; 3 | import { convertToTimeZone } from "../utils/index.js"; 4 | 5 | interface UpdateEventParams { 6 | summary?: string; 7 | description?: string; 8 | startTime?: string; 9 | endTime?: string; 10 | attendees?: string[]; 11 | } 12 | 13 | export async function updateEvent(eventId: string, updates: UpdateEventParams) { 14 | try { 15 | const auth = await getAuthClient(); 16 | const calendar = google.calendar({ version: "v3", auth }); 17 | 18 | // 先取得現有活動資料 19 | const event = await calendar.events.get({ 20 | calendarId: process.env.GOOGLE_CALENDAR_ID || "primary", 21 | eventId: eventId, 22 | }); 23 | 24 | // 準備更新的資料 25 | const updatedEvent = { 26 | ...event.data, 27 | summary: updates.summary || event.data.summary, 28 | description: updates.description || event.data.description, 29 | start: updates.startTime 30 | ? { 31 | dateTime: convertToTimeZone(updates.startTime, process.env.GOOGLE_TIME_ZONE || "Asia/Taipei"), 32 | timeZone: process.env.GOOGLE_TIME_ZONE || "Asia/Taipei", 33 | } 34 | : event.data.start, 35 | end: updates.endTime 36 | ? { 37 | dateTime: convertToTimeZone(updates.endTime, process.env.GOOGLE_TIME_ZONE || "Asia/Taipei"), 38 | timeZone: process.env.GOOGLE_TIME_ZONE || "Asia/Taipei", 39 | } 40 | : event.data.end, 41 | attendees: updates.attendees ? updates.attendees.map((email) => ({ email })) : event.data.attendees, 42 | }; 43 | 44 | const result = await calendar.events.update({ 45 | calendarId: process.env.GOOGLE_CALENDAR_ID || "primary", 46 | eventId: eventId, 47 | requestBody: updatedEvent, 48 | sendUpdates: "all", 49 | }); 50 | 51 | return { 52 | success: true, 53 | data: result.data, 54 | }; 55 | } catch (error) { 56 | return { 57 | success: false, 58 | error: error instanceof Error ? error.message : "Error updating event", 59 | }; 60 | } 61 | } 62 | ``` -------------------------------------------------------------------------------- /src/calendar-tools/calendarTools.ts: -------------------------------------------------------------------------------- ```typescript 1 | export const CREATE_EVENT_TOOL = { 2 | name: "create_event", 3 | description: "Create a new Google Calendar event", 4 | inputSchema: { 5 | type: "object", 6 | properties: { 7 | summary: { 8 | type: "string", 9 | description: "Event title", 10 | }, 11 | description: { 12 | type: "string", 13 | description: "Event description", 14 | }, 15 | startTime: { 16 | type: "string", 17 | description: "Event start time (ISO format)", 18 | }, 19 | endTime: { 20 | type: "string", 21 | description: "Event end time (ISO format)", 22 | }, 23 | attendees: { 24 | type: "array", 25 | items: { 26 | type: "string", 27 | }, 28 | description: "List of attendee email addresses", 29 | }, 30 | }, 31 | required: ["summary", "startTime", "endTime"], 32 | }, 33 | }; 34 | 35 | export const LIST_EVENTS_TOOL = { 36 | name: "list_events", 37 | description: "List Google Calendar events", 38 | inputSchema: { 39 | type: "object", 40 | properties: { 41 | timeMin: { 42 | type: "string", 43 | description: "Start time (ISO format)", 44 | }, 45 | maxResults: { 46 | type: "number", 47 | description: "Maximum number of results", 48 | }, 49 | }, 50 | }, 51 | }; 52 | 53 | export const UPDATE_EVENT_TOOL = { 54 | name: "update_event", 55 | description: "Update an existing Google Calendar event", 56 | inputSchema: { 57 | type: "object", 58 | properties: { 59 | eventId: { 60 | type: "string", 61 | description: "ID of the event to update", 62 | }, 63 | updates: { 64 | type: "object", 65 | properties: { 66 | summary: { 67 | type: "string", 68 | description: "New event title", 69 | }, 70 | description: { 71 | type: "string", 72 | description: "New event description", 73 | }, 74 | startTime: { 75 | type: "string", 76 | description: "New start time", 77 | }, 78 | endTime: { 79 | type: "string", 80 | description: "New end time", 81 | }, 82 | attendees: { 83 | type: "array", 84 | items: { 85 | type: "string", 86 | }, 87 | description: "New list of attendees", 88 | }, 89 | }, 90 | }, 91 | }, 92 | required: ["eventId", "updates"], 93 | }, 94 | }; 95 | 96 | export const DELETE_EVENT_TOOL = { 97 | name: "delete_event", 98 | description: "Delete a Google Calendar event", 99 | inputSchema: { 100 | type: "object", 101 | properties: { 102 | eventId: { 103 | type: "string", 104 | description: "ID of the event to delete", 105 | }, 106 | }, 107 | required: ["eventId"], 108 | }, 109 | }; 110 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; 6 | import { calendar_v3 } from "googleapis"; 7 | 8 | import { createEvent, deleteEvent, listEvents, tools, updateEvent } from "./calendar-tools/_index.js"; 9 | import { getAuthClient } from "./utils/auth.js"; 10 | 11 | const server = new Server( 12 | { 13 | name: "mcp-server/calendar_executor", 14 | version: "0.0.1", 15 | }, 16 | { 17 | capabilities: { 18 | description: "An MCP server providing Google Calendar integration!", 19 | tools: {}, 20 | }, 21 | } 22 | ); 23 | 24 | server.setRequestHandler(ListToolsRequestSchema, async () => ({ 25 | tools, 26 | })); 27 | 28 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 29 | try { 30 | const { name, arguments: args } = request.params; 31 | 32 | if (!args) { 33 | throw new Error("No parameters provided"); 34 | } 35 | 36 | if (name === "create_event") { 37 | const { summary, description, startTime, endTime, attendees } = args as { 38 | summary: string; 39 | description: string; 40 | startTime: string; 41 | endTime: string; 42 | attendees?: string[]; 43 | }; 44 | 45 | const result = await createEvent({ 46 | summary, 47 | description, 48 | startTime, 49 | endTime, 50 | attendees, 51 | }); 52 | 53 | if (!result.success) { 54 | return { 55 | content: [{ type: "text", text: `Error: ${result.error}` }], 56 | isError: true, 57 | }; 58 | } 59 | 60 | return { 61 | content: [{ type: "text", text: `Successfully created event: ${result.data?.summary || ""}` }], 62 | isError: false, 63 | }; 64 | } 65 | 66 | if (name === "list_events") { 67 | const { timeMin, maxResults } = args as { 68 | timeMin?: string; 69 | maxResults?: number; 70 | }; 71 | 72 | const result = await listEvents({ 73 | timeMin: timeMin || new Date().toISOString(), 74 | maxResults: maxResults || 10, 75 | }); 76 | 77 | if (!result.success) { 78 | return { 79 | content: [{ type: "text", text: `Error: ${result.error}` }], 80 | isError: true, 81 | }; 82 | } 83 | 84 | return { 85 | content: [{ type: "text", text: formatEventsList(result.data || []) }], 86 | isError: false, 87 | }; 88 | } 89 | 90 | if (name === "update_event") { 91 | const { eventId, updates } = args as { 92 | eventId: string; 93 | updates: { 94 | summary?: string; 95 | description?: string; 96 | startTime?: string; 97 | endTime?: string; 98 | attendees?: string[]; 99 | }; 100 | }; 101 | 102 | const result = await updateEvent(eventId, updates); 103 | 104 | if (!result.success) { 105 | return { 106 | content: [{ type: "text", text: `Error: ${result.error}` }], 107 | isError: true, 108 | }; 109 | } 110 | 111 | return { 112 | content: [{ type: "text", text: `Successfully updated event: ${result.data?.summary || ""}` }], 113 | isError: false, 114 | }; 115 | } 116 | 117 | if (name === "delete_event") { 118 | const { eventId } = args as { 119 | eventId: string; 120 | }; 121 | 122 | const result = await deleteEvent(eventId); 123 | 124 | if (!result.success) { 125 | return { 126 | content: [{ type: "text", text: `Error: ${result.error}` }], 127 | isError: true, 128 | }; 129 | } 130 | 131 | return { 132 | content: [{ type: "text", text: "Successfully deleted event" }], 133 | isError: false, 134 | }; 135 | } 136 | 137 | return { 138 | content: [{ type: "text", text: `Error: ${name} is an unknown tool` }], 139 | isError: true, 140 | }; 141 | } catch (error) { 142 | return { 143 | content: [ 144 | { 145 | type: "text", 146 | text: `Error: ${error instanceof Error ? error.message : String(error)}`, 147 | }, 148 | ], 149 | isError: true, 150 | }; 151 | } 152 | }); 153 | 154 | async function runServer() { 155 | try { 156 | // 先初始化認證 157 | await getAuthClient(); 158 | 159 | const transport = new StdioServerTransport(); 160 | await server.connect(transport); 161 | console.log("MCP Calendar Server started"); 162 | } catch (error) { 163 | console.error("Server startup failed:", error); 164 | process.exit(1); 165 | } 166 | } 167 | 168 | runServer().catch((error) => { 169 | console.error("Server encountered a critical error:", error); 170 | process.exit(1); 171 | }); 172 | 173 | function formatEventsList(events: calendar_v3.Schema$Event[]): string { 174 | return events 175 | .map((event) => { 176 | return ` 177 | ID: ${event.id} 178 | Event: ${event.summary} 179 | Description: ${event.description || "None"} 180 | Start Time: ${new Date(event.start?.dateTime || "").toLocaleString()} 181 | End Time: ${new Date(event.end?.dateTime || "").toLocaleString()} 182 | Attendees: ${event.attendees?.map((a: calendar_v3.Schema$EventAttendee) => a.email).join(", ") || "None"} 183 | `.trim(); 184 | }) 185 | .join("\n\n"); 186 | } 187 | ```