@iflow-mcp/v-3-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 +21 -0
- package/README.md +329 -0
- package/build/index.js +392 -0
- package/package.json +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 v-3
|
|
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,329 @@
|
|
|
1
|
+
# Google Calendar MCP Server
|
|
2
|
+
|
|
3
|
+
This MCP server allows Claude to interact with your Google Calendar, enabling capabilities like listing events, creating meetings, and finding free time slots.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Node.js (v16 or higher)
|
|
8
|
+
- Claude Desktop App
|
|
9
|
+
- A Google Cloud Project
|
|
10
|
+
- Google Calendar API enabled
|
|
11
|
+
- OAuth 2.0 credentials
|
|
12
|
+
|
|
13
|
+
## Setup Instructions
|
|
14
|
+
|
|
15
|
+
### 1. Create a Google Cloud Project
|
|
16
|
+
|
|
17
|
+
1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
|
|
18
|
+
2. Create a new project or select an existing one
|
|
19
|
+
3. Enable the Google Calendar API:
|
|
20
|
+
- Go to "APIs & Services" > "Library"
|
|
21
|
+
- Search for "Google Calendar API"
|
|
22
|
+
- Click "Enable"
|
|
23
|
+
|
|
24
|
+
### 2. Configure OAuth Consent Screen
|
|
25
|
+
|
|
26
|
+
1. Go to "APIs & Services" > "OAuth consent screen"
|
|
27
|
+
2. Select "External" user type (unless you have a Google Workspace organization)
|
|
28
|
+
3. Fill in the required information:
|
|
29
|
+
- App name
|
|
30
|
+
- User support email
|
|
31
|
+
- Developer contact information
|
|
32
|
+
4. Add the following scopes:
|
|
33
|
+
- `https://www.googleapis.com/auth/calendar`
|
|
34
|
+
- `https://www.googleapis.com/auth/calendar.events`
|
|
35
|
+
5. Add your email address as a test user
|
|
36
|
+
|
|
37
|
+
### 3. Create OAuth 2.0 Credentials
|
|
38
|
+
|
|
39
|
+
1. Go to "APIs & Services" > "Credentials"
|
|
40
|
+
2. Click "Create Credentials" > "OAuth client ID"
|
|
41
|
+
3. Select "Desktop app" as the application type
|
|
42
|
+
4. Name your client (e.g., "MCP Calendar Client")
|
|
43
|
+
5. Click "Create"
|
|
44
|
+
6. Download the client configuration file (you'll need the client ID and client secret)
|
|
45
|
+
|
|
46
|
+
### 4. Get Refresh Token
|
|
47
|
+
|
|
48
|
+
1. Create a new file named `getToken.js`:
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
const { google } = require('googleapis');
|
|
52
|
+
const http = require('http');
|
|
53
|
+
const url = require('url');
|
|
54
|
+
|
|
55
|
+
// Replace these with your OAuth 2.0 credentials
|
|
56
|
+
const CLIENT_ID = 'your-client-id';
|
|
57
|
+
const CLIENT_SECRET = 'your-client-secret';
|
|
58
|
+
const REDIRECT_URI = 'http://localhost:3000/oauth2callback';
|
|
59
|
+
|
|
60
|
+
// Configure OAuth2 client
|
|
61
|
+
const oauth2Client = new google.auth.OAuth2(
|
|
62
|
+
CLIENT_ID,
|
|
63
|
+
CLIENT_SECRET,
|
|
64
|
+
REDIRECT_URI
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Define scopes
|
|
68
|
+
const scopes = [
|
|
69
|
+
'https://www.googleapis.com/auth/calendar',
|
|
70
|
+
'https://www.googleapis.com/auth/calendar.events'
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
async function getRefreshToken() {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
try {
|
|
76
|
+
// Create server to handle OAuth callback
|
|
77
|
+
const server = http.createServer(async (req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
const queryParams = url.parse(req.url, true).query;
|
|
80
|
+
|
|
81
|
+
if (queryParams.code) {
|
|
82
|
+
// Get tokens from code
|
|
83
|
+
const { tokens } = await oauth2Client.getToken(queryParams.code);
|
|
84
|
+
console.log('\n=================');
|
|
85
|
+
console.log('Refresh Token:', tokens.refresh_token);
|
|
86
|
+
console.log('=================\n');
|
|
87
|
+
console.log('Save this refresh token in your configuration!');
|
|
88
|
+
|
|
89
|
+
// Send success response
|
|
90
|
+
res.end('Authentication successful! You can close this window.');
|
|
91
|
+
|
|
92
|
+
// Close server
|
|
93
|
+
server.close();
|
|
94
|
+
resolve(tokens);
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('Error getting tokens:', error);
|
|
98
|
+
res.end('Authentication failed! Please check console for errors.');
|
|
99
|
+
reject(error);
|
|
100
|
+
}
|
|
101
|
+
}).listen(3000, () => {
|
|
102
|
+
// Generate auth url
|
|
103
|
+
const authUrl = oauth2Client.generateAuthUrl({
|
|
104
|
+
access_type: 'offline',
|
|
105
|
+
scope: scopes,
|
|
106
|
+
prompt: 'consent' // Force consent screen to ensure refresh token
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
console.log('1. Copy this URL and paste it in your browser:');
|
|
110
|
+
console.log('\n', authUrl, '\n');
|
|
111
|
+
console.log('2. Follow the Google authentication process');
|
|
112
|
+
console.log('3. Wait for the refresh token to appear here');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('Server creation error:', error);
|
|
117
|
+
reject(error);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Run the token retrieval
|
|
123
|
+
getRefreshToken().catch(console.error);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
2. Install required dependency:
|
|
127
|
+
```bash
|
|
128
|
+
npm install googleapis
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
3. Update the script with your OAuth credentials:
|
|
132
|
+
- Replace `your-client-id` with your actual client ID
|
|
133
|
+
- Replace `your-client-secret` with your actual client secret
|
|
134
|
+
|
|
135
|
+
4. Run the script:
|
|
136
|
+
```bash
|
|
137
|
+
node getToken.js
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
5. Follow the instructions in the console:
|
|
141
|
+
- Copy the provided URL
|
|
142
|
+
- Paste it into your browser
|
|
143
|
+
- Complete the Google authentication process
|
|
144
|
+
- Copy the refresh token that appears in the console
|
|
145
|
+
|
|
146
|
+
### 5. Configure Claude Desktop
|
|
147
|
+
|
|
148
|
+
1. Open your Claude Desktop configuration file:
|
|
149
|
+
|
|
150
|
+
**For MacOS:**
|
|
151
|
+
```bash
|
|
152
|
+
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**For Windows:**
|
|
156
|
+
```bash
|
|
157
|
+
code %AppData%\Claude\claude_desktop_config.json
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
2. Add or update the configuration:
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"mcpServers": {
|
|
164
|
+
"google-calendar": {
|
|
165
|
+
"command": "node",
|
|
166
|
+
"args": [
|
|
167
|
+
"/ABSOLUTE/PATH/TO/YOUR/build/index.js"
|
|
168
|
+
],
|
|
169
|
+
"env": {
|
|
170
|
+
"GOOGLE_CLIENT_ID": "your_client_id_here",
|
|
171
|
+
"GOOGLE_CLIENT_SECRET": "your_client_secret_here",
|
|
172
|
+
"GOOGLE_REDIRECT_URI": "http://localhost",
|
|
173
|
+
"GOOGLE_REFRESH_TOKEN": "your_refresh_token_here"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
3. Save the file and restart Claude Desktop
|
|
181
|
+
|
|
182
|
+
## Initial Project Setup
|
|
183
|
+
|
|
184
|
+
1. Create a new directory for your project:
|
|
185
|
+
```bash
|
|
186
|
+
mkdir google-calendar-mcp
|
|
187
|
+
cd google-calendar-mcp
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
2. Initialize a new npm project:
|
|
191
|
+
```bash
|
|
192
|
+
npm init -y
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
3. Install dependencies:
|
|
196
|
+
```bash
|
|
197
|
+
npm install @modelcontextprotocol/sdk googleapis google-auth-library zod
|
|
198
|
+
npm install -D @types/node typescript
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
4. Create a tsconfig.json file:
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"compilerOptions": {
|
|
205
|
+
"target": "ES2022",
|
|
206
|
+
"module": "Node16",
|
|
207
|
+
"moduleResolution": "Node16",
|
|
208
|
+
"outDir": "./build",
|
|
209
|
+
"rootDir": "./src",
|
|
210
|
+
"strict": true,
|
|
211
|
+
"esModuleInterop": true,
|
|
212
|
+
"skipLibCheck": true,
|
|
213
|
+
"forceConsistentCasingInFileNames": true
|
|
214
|
+
},
|
|
215
|
+
"include": ["src/**/*"],
|
|
216
|
+
"exclude": ["node_modules"]
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
5. Update package.json:
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"type": "module",
|
|
224
|
+
"scripts": {
|
|
225
|
+
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\""
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
6. Create your source directory:
|
|
231
|
+
```bash
|
|
232
|
+
mkdir src
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
7. Create a .env file for local development (don't commit this file):
|
|
236
|
+
```bash
|
|
237
|
+
GOOGLE_CLIENT_ID=your_client_id_here
|
|
238
|
+
GOOGLE_CLIENT_SECRET=your_client_secret_here
|
|
239
|
+
GOOGLE_REDIRECT_URI=http://localhost
|
|
240
|
+
GOOGLE_REFRESH_TOKEN=your_refresh_token_here
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Building and Running
|
|
244
|
+
|
|
245
|
+
1. Build the server:
|
|
246
|
+
```bash
|
|
247
|
+
npm run build
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
2. The server will automatically start when you open Claude Desktop
|
|
251
|
+
|
|
252
|
+
## Available Tools
|
|
253
|
+
|
|
254
|
+
The server provides the following tools:
|
|
255
|
+
|
|
256
|
+
1. `list_events`: List calendar events within a specified time range
|
|
257
|
+
2. `create_event`: Create a new calendar event
|
|
258
|
+
3. `update_event`: Update an existing calendar event
|
|
259
|
+
4. `delete_event`: Delete a calendar event
|
|
260
|
+
5. `find_free_time`: Find available time slots in the calendar
|
|
261
|
+
|
|
262
|
+
## Example Usage in Claude
|
|
263
|
+
|
|
264
|
+
After setup, you can use commands like:
|
|
265
|
+
|
|
266
|
+
- "Show me my calendar events for next week"
|
|
267
|
+
- "Schedule a meeting with [email_id] tomorrow at 2 PM for 1 hour"
|
|
268
|
+
- "Find a free 30-minute slot this afternoon"
|
|
269
|
+
- "Update my 3 PM meeting to 4 PM"
|
|
270
|
+
- "Cancel my meeting with ID [event_id]"
|
|
271
|
+
|
|
272
|
+
## Troubleshooting
|
|
273
|
+
|
|
274
|
+
### Common Issues
|
|
275
|
+
|
|
276
|
+
1. **Tools not appearing in Claude:**
|
|
277
|
+
- Check Claude Desktop logs: `tail -f ~/Library/Logs/Claude/mcp*.log`
|
|
278
|
+
- Verify all environment variables are set correctly
|
|
279
|
+
- Ensure the path to index.js is absolute and correct
|
|
280
|
+
|
|
281
|
+
2. **Authentication Errors:**
|
|
282
|
+
- Verify your OAuth credentials are correct
|
|
283
|
+
- Check if refresh token is valid
|
|
284
|
+
- Ensure required scopes are enabled
|
|
285
|
+
|
|
286
|
+
3. **Server Connection Issues:**
|
|
287
|
+
- Check if the server built successfully
|
|
288
|
+
- Verify file permissions on build/index.js (should be 755)
|
|
289
|
+
- Try running the server directly: `node /path/to/build/index.js`
|
|
290
|
+
|
|
291
|
+
### Viewing Logs
|
|
292
|
+
|
|
293
|
+
To view server logs:
|
|
294
|
+
```bash
|
|
295
|
+
# For MacOS/Linux:
|
|
296
|
+
tail -n 20 -f ~/Library/Logs/Claude/mcp*.log
|
|
297
|
+
|
|
298
|
+
# For Windows:
|
|
299
|
+
Get-Content -Path "$env:AppData\Claude\Logs\mcp*.log" -Wait -Tail 20
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Environment Variables
|
|
303
|
+
|
|
304
|
+
If you're getting environment variable errors, verify each one:
|
|
305
|
+
|
|
306
|
+
1. GOOGLE_CLIENT_ID: Should start with something like "123456789-..."
|
|
307
|
+
2. GOOGLE_CLIENT_SECRET: Usually ends in ".apps.googleusercontent.com"
|
|
308
|
+
3. GOOGLE_REDIRECT_URI: Should be "http://localhost"
|
|
309
|
+
4. GOOGLE_REFRESH_TOKEN: A long string that doesn't expire
|
|
310
|
+
|
|
311
|
+
## Security Considerations
|
|
312
|
+
|
|
313
|
+
- Keep your OAuth credentials secure
|
|
314
|
+
- Don't commit credentials to version control
|
|
315
|
+
- Use environment variables for sensitive data
|
|
316
|
+
- Regularly rotate refresh tokens
|
|
317
|
+
- Monitor API usage in Google Cloud Console
|
|
318
|
+
|
|
319
|
+
## License
|
|
320
|
+
|
|
321
|
+
MIT License - See LICENSE file for details.
|
|
322
|
+
|
|
323
|
+
## Support
|
|
324
|
+
|
|
325
|
+
If you encounter any issues:
|
|
326
|
+
1. Check the troubleshooting section above
|
|
327
|
+
2. Review Claude Desktop logs
|
|
328
|
+
3. Open an issue on GitHub
|
|
329
|
+
4. Contact the maintainer
|
package/build/index.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { google } from 'googleapis';
|
|
7
|
+
import { OAuth2Client } from 'google-auth-library';
|
|
8
|
+
// Initialize Google Calendar client
|
|
9
|
+
const oauth2Client = new OAuth2Client({
|
|
10
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
11
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
12
|
+
redirectUri: process.env.GOOGLE_REDIRECT_URI,
|
|
13
|
+
});
|
|
14
|
+
// Set credentials from environment variables
|
|
15
|
+
oauth2Client.setCredentials({
|
|
16
|
+
refresh_token: process.env.GOOGLE_REFRESH_TOKEN,
|
|
17
|
+
});
|
|
18
|
+
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
|
19
|
+
// Validation schemas
|
|
20
|
+
const schemas = {
|
|
21
|
+
toolInputs: {
|
|
22
|
+
listEvents: z.object({
|
|
23
|
+
timeMin: z.string().optional(),
|
|
24
|
+
timeMax: z.string().optional(),
|
|
25
|
+
maxResults: z.number().optional(),
|
|
26
|
+
}),
|
|
27
|
+
createEvent: z.object({
|
|
28
|
+
summary: z.string(),
|
|
29
|
+
description: z.string().optional(),
|
|
30
|
+
startTime: z.string(),
|
|
31
|
+
endTime: z.string(),
|
|
32
|
+
attendees: z.array(z.string()).optional(),
|
|
33
|
+
}),
|
|
34
|
+
updateEvent: z.object({
|
|
35
|
+
eventId: z.string(),
|
|
36
|
+
summary: z.string().optional(),
|
|
37
|
+
description: z.string().optional(),
|
|
38
|
+
startTime: z.string().optional(),
|
|
39
|
+
endTime: z.string().optional(),
|
|
40
|
+
}),
|
|
41
|
+
deleteEvent: z.object({
|
|
42
|
+
eventId: z.string(),
|
|
43
|
+
}),
|
|
44
|
+
findFreeTime: z.object({
|
|
45
|
+
timeMin: z.string(),
|
|
46
|
+
timeMax: z.string(),
|
|
47
|
+
duration: z.number(), // duration in minutes
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
// Tool definitions
|
|
52
|
+
const TOOL_DEFINITIONS = [
|
|
53
|
+
{
|
|
54
|
+
name: "list_events",
|
|
55
|
+
description: "List calendar events within a specified time range",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
timeMin: {
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "Start time (ISO string)",
|
|
62
|
+
},
|
|
63
|
+
timeMax: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "End time (ISO string)",
|
|
66
|
+
},
|
|
67
|
+
maxResults: {
|
|
68
|
+
type: "number",
|
|
69
|
+
description: "Maximum number of events to return",
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "create_event",
|
|
76
|
+
description: "Create a new calendar event",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
summary: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "Event title",
|
|
83
|
+
},
|
|
84
|
+
description: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "Event description",
|
|
87
|
+
},
|
|
88
|
+
startTime: {
|
|
89
|
+
type: "string",
|
|
90
|
+
description: "Event start time (ISO string)",
|
|
91
|
+
},
|
|
92
|
+
endTime: {
|
|
93
|
+
type: "string",
|
|
94
|
+
description: "Event end time (ISO string)",
|
|
95
|
+
},
|
|
96
|
+
attendees: {
|
|
97
|
+
type: "array",
|
|
98
|
+
items: {
|
|
99
|
+
type: "string",
|
|
100
|
+
},
|
|
101
|
+
description: "List of attendee email addresses",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
required: ["summary", "startTime", "endTime"],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "update_event",
|
|
109
|
+
description: "Update an existing calendar event",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
eventId: {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: "ID of the event to update",
|
|
116
|
+
},
|
|
117
|
+
summary: {
|
|
118
|
+
type: "string",
|
|
119
|
+
description: "New event title",
|
|
120
|
+
},
|
|
121
|
+
description: {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: "New event description",
|
|
124
|
+
},
|
|
125
|
+
startTime: {
|
|
126
|
+
type: "string",
|
|
127
|
+
description: "New start time (ISO string)",
|
|
128
|
+
},
|
|
129
|
+
endTime: {
|
|
130
|
+
type: "string",
|
|
131
|
+
description: "New end time (ISO string)",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
required: ["eventId"],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "delete_event",
|
|
139
|
+
description: "Delete a calendar event",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {
|
|
143
|
+
eventId: {
|
|
144
|
+
type: "string",
|
|
145
|
+
description: "ID of the event to delete",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
required: ["eventId"],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "find_free_time",
|
|
153
|
+
description: "Find available time slots in the calendar",
|
|
154
|
+
inputSchema: {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
timeMin: {
|
|
158
|
+
type: "string",
|
|
159
|
+
description: "Start of time range (ISO string)",
|
|
160
|
+
},
|
|
161
|
+
timeMax: {
|
|
162
|
+
type: "string",
|
|
163
|
+
description: "End of time range (ISO string)",
|
|
164
|
+
},
|
|
165
|
+
duration: {
|
|
166
|
+
type: "number",
|
|
167
|
+
description: "Desired duration in minutes",
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
required: ["timeMin", "timeMax", "duration"],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
// Tool implementation handlers
|
|
175
|
+
const toolHandlers = {
|
|
176
|
+
async list_events(args) {
|
|
177
|
+
const { timeMin, timeMax, maxResults = 10 } = schemas.toolInputs.listEvents.parse(args);
|
|
178
|
+
const response = await calendar.events.list({
|
|
179
|
+
calendarId: 'primary',
|
|
180
|
+
timeMin: timeMin || new Date().toISOString(),
|
|
181
|
+
timeMax,
|
|
182
|
+
maxResults,
|
|
183
|
+
singleEvents: true,
|
|
184
|
+
orderBy: 'startTime',
|
|
185
|
+
});
|
|
186
|
+
const events = response.data.items || [];
|
|
187
|
+
const formattedEvents = events.map(event => {
|
|
188
|
+
return `• ${event.summary}\n Start: ${event.start?.dateTime || event.start?.date}\n End: ${event.end?.dateTime || event.end?.date}\n ID: ${event.id}`;
|
|
189
|
+
}).join('\n\n');
|
|
190
|
+
return {
|
|
191
|
+
content: [{
|
|
192
|
+
type: "text",
|
|
193
|
+
text: events.length ?
|
|
194
|
+
`Found ${events.length} events:\n\n${formattedEvents}` :
|
|
195
|
+
"No events found in the specified time range."
|
|
196
|
+
}]
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
async create_event(args) {
|
|
200
|
+
const { summary, description, startTime, endTime, attendees } = schemas.toolInputs.createEvent.parse(args);
|
|
201
|
+
const event = await calendar.events.insert({
|
|
202
|
+
calendarId: 'primary',
|
|
203
|
+
requestBody: {
|
|
204
|
+
summary,
|
|
205
|
+
description,
|
|
206
|
+
start: {
|
|
207
|
+
dateTime: startTime,
|
|
208
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
209
|
+
},
|
|
210
|
+
end: {
|
|
211
|
+
dateTime: endTime,
|
|
212
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
213
|
+
},
|
|
214
|
+
attendees: attendees?.map(email => ({ email })),
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
return {
|
|
218
|
+
content: [{
|
|
219
|
+
type: "text",
|
|
220
|
+
text: `Event created successfully!\nID: ${event.data.id}\nLink: ${event.data.htmlLink}`
|
|
221
|
+
}]
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
async update_event(args) {
|
|
225
|
+
const { eventId, summary, description, startTime, endTime } = schemas.toolInputs.updateEvent.parse(args);
|
|
226
|
+
// Get existing event
|
|
227
|
+
const existingEvent = await calendar.events.get({
|
|
228
|
+
calendarId: 'primary',
|
|
229
|
+
eventId,
|
|
230
|
+
});
|
|
231
|
+
// Prepare update payload
|
|
232
|
+
const updatePayload = {
|
|
233
|
+
summary: summary || existingEvent.data.summary,
|
|
234
|
+
description: description || existingEvent.data.description,
|
|
235
|
+
};
|
|
236
|
+
if (startTime) {
|
|
237
|
+
updatePayload.start = {
|
|
238
|
+
dateTime: startTime,
|
|
239
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (endTime) {
|
|
243
|
+
updatePayload.end = {
|
|
244
|
+
dateTime: endTime,
|
|
245
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
await calendar.events.update({
|
|
249
|
+
calendarId: 'primary',
|
|
250
|
+
eventId,
|
|
251
|
+
requestBody: updatePayload,
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
content: [{
|
|
255
|
+
type: "text",
|
|
256
|
+
text: `Event ${eventId} updated successfully!`
|
|
257
|
+
}]
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
async delete_event(args) {
|
|
261
|
+
const { eventId } = schemas.toolInputs.deleteEvent.parse(args);
|
|
262
|
+
await calendar.events.delete({
|
|
263
|
+
calendarId: 'primary',
|
|
264
|
+
eventId,
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
content: [{
|
|
268
|
+
type: "text",
|
|
269
|
+
text: `Event ${eventId} deleted successfully!`
|
|
270
|
+
}]
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
async find_free_time(args) {
|
|
274
|
+
const { timeMin, timeMax, duration } = schemas.toolInputs.findFreeTime.parse(args);
|
|
275
|
+
// Get existing events in the time range
|
|
276
|
+
const response = await calendar.events.list({
|
|
277
|
+
calendarId: 'primary',
|
|
278
|
+
timeMin,
|
|
279
|
+
timeMax,
|
|
280
|
+
singleEvents: true,
|
|
281
|
+
orderBy: 'startTime',
|
|
282
|
+
});
|
|
283
|
+
const events = response.data.items || [];
|
|
284
|
+
const freeTimes = [];
|
|
285
|
+
let currentTime = new Date(timeMin);
|
|
286
|
+
const endTime = new Date(timeMax);
|
|
287
|
+
const durationMs = duration * 60000; // Convert minutes to milliseconds
|
|
288
|
+
// Find free time slots
|
|
289
|
+
for (const event of events) {
|
|
290
|
+
const eventStart = new Date(event.start?.dateTime || event.start?.date || '');
|
|
291
|
+
// Check if there's enough time before the event
|
|
292
|
+
if (eventStart.getTime() - currentTime.getTime() >= durationMs) {
|
|
293
|
+
freeTimes.push({
|
|
294
|
+
start: currentTime.toISOString(),
|
|
295
|
+
end: new Date(eventStart.getTime() - 1).toISOString(),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
currentTime = new Date(event.end?.dateTime || event.end?.date || '');
|
|
299
|
+
}
|
|
300
|
+
// Check for free time after the last event
|
|
301
|
+
if (endTime.getTime() - currentTime.getTime() >= durationMs) {
|
|
302
|
+
freeTimes.push({
|
|
303
|
+
start: currentTime.toISOString(),
|
|
304
|
+
end: endTime.toISOString(),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const formattedTimes = freeTimes.map(slot => `• ${new Date(slot.start).toLocaleString()} - ${new Date(slot.end).toLocaleString()}`).join('\n');
|
|
308
|
+
return {
|
|
309
|
+
content: [{
|
|
310
|
+
type: "text",
|
|
311
|
+
text: freeTimes.length ?
|
|
312
|
+
`Found ${freeTimes.length} available time slots:\n\n${formattedTimes}` :
|
|
313
|
+
`No available time slots found for duration of ${duration} minutes in the specified range.`
|
|
314
|
+
}]
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
// Initialize MCP server
|
|
319
|
+
const server = new Server({
|
|
320
|
+
name: "google-calendar-server",
|
|
321
|
+
version: "1.0.0",
|
|
322
|
+
}, {
|
|
323
|
+
capabilities: {
|
|
324
|
+
tools: {},
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
// Register tool handlers
|
|
328
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
329
|
+
console.error("Tools requested by client");
|
|
330
|
+
return { tools: TOOL_DEFINITIONS };
|
|
331
|
+
});
|
|
332
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
333
|
+
console.error("Tools requested by client");
|
|
334
|
+
console.error("Returning tools:", JSON.stringify(TOOL_DEFINITIONS, null, 2));
|
|
335
|
+
return { tools: TOOL_DEFINITIONS };
|
|
336
|
+
});
|
|
337
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
338
|
+
const { name, arguments: args } = request.params;
|
|
339
|
+
try {
|
|
340
|
+
const handler = toolHandlers[name];
|
|
341
|
+
if (!handler) {
|
|
342
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
343
|
+
}
|
|
344
|
+
return await handler(args);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
console.error(`Error executing tool ${name}:`, error);
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
// Start the server
|
|
352
|
+
async function main() {
|
|
353
|
+
try {
|
|
354
|
+
// Check for required environment variables (skip in test mode)
|
|
355
|
+
const isTestMode = process.env.MCP_TEST_MODE === 'true';
|
|
356
|
+
const requiredEnvVars = [
|
|
357
|
+
'GOOGLE_CLIENT_ID',
|
|
358
|
+
'GOOGLE_CLIENT_SECRET',
|
|
359
|
+
'GOOGLE_REDIRECT_URI',
|
|
360
|
+
'GOOGLE_REFRESH_TOKEN'
|
|
361
|
+
];
|
|
362
|
+
const missingVars = requiredEnvVars.filter(varName => !process.env[varName]);
|
|
363
|
+
if (missingVars.length > 0 && !isTestMode) {
|
|
364
|
+
console.error(`Missing required environment variables: ${missingVars.join(', ')}`);
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
if (isTestMode) {
|
|
368
|
+
console.error("Running in test mode - skipping environment variable validation");
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
console.error("Starting server with env vars:", {
|
|
372
|
+
clientId: process.env.GOOGLE_CLIENT_ID?.substring(0, 5) + '...',
|
|
373
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET?.substring(0, 5) + '...',
|
|
374
|
+
redirectUri: process.env.GOOGLE_REDIRECT_URI,
|
|
375
|
+
hasRefreshToken: !!process.env.GOOGLE_REFRESH_TOKEN
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
const transport = new StdioServerTransport();
|
|
379
|
+
console.error("Created transport");
|
|
380
|
+
await server.connect(transport);
|
|
381
|
+
console.error("Connected to transport");
|
|
382
|
+
console.error("Google Calendar MCP Server running on stdio");
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
console.error("Startup error:", error);
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
main().catch((error) => {
|
|
390
|
+
console.error("Fatal error:", error);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@iflow-mcp/v-3-google-calendar",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"iflow-mcp_v-3-google-calendar": "./build/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [],
|
|
14
|
+
"author": "",
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"description": "",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
19
|
+
"google-auth-library": "^9.15.0",
|
|
20
|
+
"googleapis": "^144.0.0",
|
|
21
|
+
"open": "^10.1.0",
|
|
22
|
+
"zod": "^3.24.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^22.10.2",
|
|
26
|
+
"typescript": "^5.7.2"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"build"
|
|
30
|
+
]
|
|
31
|
+
}
|