mcpeasy 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/.claudeignore +0 -3
- data/.mcp.json +10 -1
- data/CHANGELOG.md +50 -0
- data/CLAUDE.md +19 -5
- data/README.md +19 -3
- data/lib/mcpeasy/cli.rb +33 -10
- data/lib/mcpeasy/config.rb +22 -1
- data/lib/mcpeasy/setup.rb +1 -0
- data/lib/mcpeasy/version.rb +1 -1
- data/lib/utilities/gcal/README.md +11 -3
- data/lib/utilities/gcal/cli.rb +110 -108
- data/lib/utilities/gcal/mcp.rb +463 -308
- data/lib/utilities/gcal/service.rb +312 -0
- data/lib/utilities/gdrive/README.md +3 -3
- data/lib/utilities/gdrive/cli.rb +98 -96
- data/lib/utilities/gdrive/mcp.rb +290 -288
- data/lib/utilities/gdrive/service.rb +293 -0
- data/lib/utilities/gmeet/cli.rb +131 -129
- data/lib/utilities/gmeet/mcp.rb +374 -372
- data/lib/utilities/gmeet/service.rb +409 -0
- data/lib/utilities/notion/README.md +287 -0
- data/lib/utilities/notion/cli.rb +245 -0
- data/lib/utilities/notion/mcp.rb +607 -0
- data/lib/utilities/notion/service.rb +327 -0
- data/lib/utilities/slack/README.md +3 -3
- data/lib/utilities/slack/cli.rb +69 -54
- data/lib/utilities/slack/mcp.rb +277 -226
- data/lib/utilities/slack/service.rb +134 -0
- metadata +11 -8
- data/env.template +0 -11
- data/lib/utilities/gcal/gcal_tool.rb +0 -308
- data/lib/utilities/gdrive/gdrive_tool.rb +0 -291
- data/lib/utilities/gmeet/gmeet_tool.rb +0 -407
- data/lib/utilities/slack/slack_tool.rb +0 -119
- data/logs/.keep +0 -0
data/lib/utilities/gcal/mcp.rb
CHANGED
@@ -3,379 +3,534 @@
|
|
3
3
|
|
4
4
|
require "bundler/setup"
|
5
5
|
require "json"
|
6
|
-
require_relative "
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
"list_events" => {
|
21
|
-
name: "list_events",
|
22
|
-
description: "List calendar events with optional date filtering",
|
23
|
-
inputSchema: {
|
24
|
-
type: "object",
|
25
|
-
properties: {
|
26
|
-
start_date: {
|
27
|
-
type: "string",
|
28
|
-
description: "Start date in YYYY-MM-DD format (default: today)"
|
29
|
-
},
|
30
|
-
end_date: {
|
31
|
-
type: "string",
|
32
|
-
description: "End date in YYYY-MM-DD format (default: 7 days from start)"
|
33
|
-
},
|
34
|
-
max_results: {
|
35
|
-
type: "number",
|
36
|
-
description: "Maximum number of events to return (default: 20)"
|
37
|
-
},
|
38
|
-
calendar_id: {
|
39
|
-
type: "string",
|
40
|
-
description: "Calendar ID to list events from (default: primary calendar)"
|
6
|
+
require_relative "service"
|
7
|
+
|
8
|
+
module Gcal
|
9
|
+
class MCPServer
|
10
|
+
def initialize
|
11
|
+
@prompts = [
|
12
|
+
{
|
13
|
+
name: "check_schedule",
|
14
|
+
description: "Check your calendar schedule for a specific time period",
|
15
|
+
arguments: [
|
16
|
+
{
|
17
|
+
name: "period",
|
18
|
+
description: "Time period to check (e.g., 'today', 'tomorrow', 'this week', 'next Monday')",
|
19
|
+
required: true
|
41
20
|
}
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
21
|
+
]
|
22
|
+
},
|
23
|
+
{
|
24
|
+
name: "find_meeting",
|
25
|
+
description: "Find a specific meeting or event by searching for keywords",
|
26
|
+
arguments: [
|
27
|
+
{
|
28
|
+
name: "keywords",
|
29
|
+
description: "Keywords to search for in event titles and descriptions",
|
30
|
+
required: true
|
31
|
+
}
|
32
|
+
]
|
33
|
+
},
|
34
|
+
{
|
35
|
+
name: "check_availability",
|
36
|
+
description: "Check if you're free at a specific time",
|
37
|
+
arguments: [
|
38
|
+
{
|
39
|
+
name: "datetime",
|
40
|
+
description: "Date and time to check (e.g., 'tomorrow at 2pm', 'next Tuesday afternoon')",
|
41
|
+
required: true
|
42
|
+
}
|
43
|
+
]
|
44
|
+
},
|
45
|
+
{
|
46
|
+
name: "weekly_overview",
|
47
|
+
description: "Get an overview of your schedule for the upcoming week",
|
48
|
+
arguments: []
|
49
|
+
},
|
50
|
+
{
|
51
|
+
name: "meeting_conflicts",
|
52
|
+
description: "Check for any overlapping or back-to-back meetings",
|
53
|
+
arguments: [
|
54
|
+
{
|
55
|
+
name: "date",
|
56
|
+
description: "Date to check for conflicts (default: today)",
|
57
|
+
required: false
|
58
|
+
}
|
59
|
+
]
|
53
60
|
}
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
61
|
+
]
|
62
|
+
|
63
|
+
@tools = {
|
64
|
+
"test_connection" => {
|
65
|
+
name: "test_connection",
|
66
|
+
description: "Test the Google Calendar API connection",
|
67
|
+
inputSchema: {
|
68
|
+
type: "object",
|
69
|
+
properties: {},
|
70
|
+
required: []
|
71
|
+
}
|
72
|
+
},
|
73
|
+
"list_events" => {
|
74
|
+
name: "list_events",
|
75
|
+
description: "List calendar events with optional date filtering",
|
76
|
+
inputSchema: {
|
77
|
+
type: "object",
|
78
|
+
properties: {
|
79
|
+
start_date: {
|
80
|
+
type: "string",
|
81
|
+
description: "Start date in YYYY-MM-DD format (default: today)"
|
82
|
+
},
|
83
|
+
end_date: {
|
84
|
+
type: "string",
|
85
|
+
description: "End date in YYYY-MM-DD format (default: 7 days from start)"
|
86
|
+
},
|
87
|
+
max_results: {
|
88
|
+
type: "number",
|
89
|
+
description: "Maximum number of events to return (default: 20)"
|
90
|
+
},
|
91
|
+
calendar_id: {
|
92
|
+
type: "string",
|
93
|
+
description: "Calendar ID to list events from (default: primary calendar)"
|
94
|
+
}
|
68
95
|
},
|
69
|
-
|
70
|
-
|
71
|
-
|
96
|
+
required: []
|
97
|
+
}
|
98
|
+
},
|
99
|
+
"list_calendars" => {
|
100
|
+
name: "list_calendars",
|
101
|
+
description: "List available calendars",
|
102
|
+
inputSchema: {
|
103
|
+
type: "object",
|
104
|
+
properties: {},
|
105
|
+
required: []
|
106
|
+
}
|
107
|
+
},
|
108
|
+
"search_events" => {
|
109
|
+
name: "search_events",
|
110
|
+
description: "Search for events by text content",
|
111
|
+
inputSchema: {
|
112
|
+
type: "object",
|
113
|
+
properties: {
|
114
|
+
query: {
|
115
|
+
type: "string",
|
116
|
+
description: "Search query to find events"
|
117
|
+
},
|
118
|
+
start_date: {
|
119
|
+
type: "string",
|
120
|
+
description: "Start date in YYYY-MM-DD format (default: today)"
|
121
|
+
},
|
122
|
+
end_date: {
|
123
|
+
type: "string",
|
124
|
+
description: "End date in YYYY-MM-DD format (default: 30 days from start)"
|
125
|
+
},
|
126
|
+
max_results: {
|
127
|
+
type: "number",
|
128
|
+
description: "Maximum number of events to return (default: 10)"
|
129
|
+
}
|
72
130
|
},
|
73
|
-
|
74
|
-
|
75
|
-
description: "Maximum number of events to return (default: 10)"
|
76
|
-
}
|
77
|
-
},
|
78
|
-
required: ["query"]
|
131
|
+
required: ["query"]
|
132
|
+
}
|
79
133
|
}
|
80
134
|
}
|
81
|
-
|
82
|
-
end
|
135
|
+
end
|
83
136
|
|
84
|
-
|
85
|
-
|
86
|
-
|
137
|
+
def run
|
138
|
+
# Disable stdout buffering for immediate response
|
139
|
+
$stdout.sync = true
|
87
140
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
141
|
+
# Log startup to file instead of stdout to avoid protocol interference
|
142
|
+
Mcpeasy::Config.ensure_config_dirs
|
143
|
+
File.write(Mcpeasy::Config.log_file_path("gcal", "startup"), "#{Time.now}: Google Calendar MCP Server starting on stdio\n", mode: "a")
|
144
|
+
while (line = $stdin.gets)
|
145
|
+
handle_request(line.strip)
|
146
|
+
end
|
147
|
+
rescue Interrupt
|
148
|
+
# Silent shutdown
|
149
|
+
rescue => e
|
150
|
+
# Log to a file instead of stderr to avoid protocol interference
|
151
|
+
File.write(Mcpeasy::Config.log_file_path("gcal", "error"), "#{Time.now}: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
|
93
152
|
end
|
94
|
-
rescue Interrupt
|
95
|
-
# Silent shutdown
|
96
|
-
rescue => e
|
97
|
-
# Log to a file instead of stderr to avoid protocol interference
|
98
|
-
File.write(Mcpeasy::Config.log_file_path("gcal", "error"), "#{Time.now}: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
|
99
|
-
end
|
100
153
|
|
101
|
-
|
154
|
+
private
|
102
155
|
|
103
|
-
|
104
|
-
|
156
|
+
def handle_request(line)
|
157
|
+
return if line.empty?
|
105
158
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
159
|
+
begin
|
160
|
+
request = JSON.parse(line)
|
161
|
+
response = process_request(request)
|
162
|
+
if response
|
163
|
+
puts JSON.generate(response)
|
164
|
+
$stdout.flush
|
165
|
+
end
|
166
|
+
rescue JSON::ParserError => e
|
167
|
+
error_response = {
|
168
|
+
jsonrpc: "2.0",
|
169
|
+
id: nil,
|
170
|
+
error: {
|
171
|
+
code: -32700,
|
172
|
+
message: "Parse error",
|
173
|
+
data: e.message
|
174
|
+
}
|
175
|
+
}
|
176
|
+
puts JSON.generate(error_response)
|
111
177
|
$stdout.flush
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
178
|
+
rescue => e
|
179
|
+
File.write(Mcpeasy::Config.log_file_path("gcal", "error"), "#{Time.now}: Error handling request: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
|
180
|
+
error_response = {
|
181
|
+
jsonrpc: "2.0",
|
182
|
+
id: request&.dig("id"),
|
183
|
+
error: {
|
184
|
+
code: -32603,
|
185
|
+
message: "Internal error",
|
186
|
+
data: e.message
|
187
|
+
}
|
121
188
|
}
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
189
|
+
puts JSON.generate(error_response)
|
190
|
+
$stdout.flush
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def process_request(request)
|
195
|
+
id = request["id"]
|
196
|
+
method = request["method"]
|
197
|
+
params = request["params"] || {}
|
198
|
+
|
199
|
+
case method
|
200
|
+
when "notifications/initialized"
|
201
|
+
# Client acknowledgment - no response needed
|
202
|
+
nil
|
203
|
+
when "initialize"
|
204
|
+
initialize_response(id, params)
|
205
|
+
when "tools/list"
|
206
|
+
tools_list_response(id, params)
|
207
|
+
when "tools/call"
|
208
|
+
tools_call_response(id, params)
|
209
|
+
when "prompts/list"
|
210
|
+
prompts_list_response(id, params)
|
211
|
+
when "prompts/get"
|
212
|
+
prompts_get_response(id, params)
|
213
|
+
else
|
214
|
+
{
|
215
|
+
jsonrpc: "2.0",
|
216
|
+
id: id,
|
217
|
+
error: {
|
218
|
+
code: -32601,
|
219
|
+
message: "Method not found",
|
220
|
+
data: "Unknown method: #{method}"
|
221
|
+
}
|
134
222
|
}
|
135
|
-
|
136
|
-
puts JSON.generate(error_response)
|
137
|
-
$stdout.flush
|
223
|
+
end
|
138
224
|
end
|
139
|
-
end
|
140
225
|
|
141
|
-
|
142
|
-
id = request["id"]
|
143
|
-
method = request["method"]
|
144
|
-
params = request["params"] || {}
|
145
|
-
|
146
|
-
case method
|
147
|
-
when "notifications/initialized"
|
148
|
-
# Client acknowledgment - no response needed
|
149
|
-
nil
|
150
|
-
when "initialize"
|
151
|
-
initialize_response(id, params)
|
152
|
-
when "tools/list"
|
153
|
-
tools_list_response(id, params)
|
154
|
-
when "tools/call"
|
155
|
-
tools_call_response(id, params)
|
156
|
-
else
|
226
|
+
def initialize_response(id, params)
|
157
227
|
{
|
158
228
|
jsonrpc: "2.0",
|
159
229
|
id: id,
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
230
|
+
result: {
|
231
|
+
protocolVersion: "2024-11-05",
|
232
|
+
capabilities: {
|
233
|
+
tools: {},
|
234
|
+
prompts: {}
|
235
|
+
},
|
236
|
+
serverInfo: {
|
237
|
+
name: "gcal-mcp-server",
|
238
|
+
version: "1.0.0"
|
239
|
+
}
|
164
240
|
}
|
165
241
|
}
|
166
242
|
end
|
167
|
-
end
|
168
243
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
capabilities: {
|
176
|
-
tools: {}
|
177
|
-
},
|
178
|
-
serverInfo: {
|
179
|
-
name: "gcal-mcp-server",
|
180
|
-
version: "1.0.0"
|
244
|
+
def tools_list_response(id, params)
|
245
|
+
{
|
246
|
+
jsonrpc: "2.0",
|
247
|
+
id: id,
|
248
|
+
result: {
|
249
|
+
tools: @tools.values
|
181
250
|
}
|
182
251
|
}
|
183
|
-
|
184
|
-
end
|
252
|
+
end
|
185
253
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
id: id,
|
190
|
-
result: {
|
191
|
-
tools: @tools.values
|
192
|
-
}
|
193
|
-
}
|
194
|
-
end
|
254
|
+
def tools_call_response(id, params)
|
255
|
+
tool_name = params["name"]
|
256
|
+
arguments = params["arguments"] || {}
|
195
257
|
|
196
|
-
|
197
|
-
|
198
|
-
|
258
|
+
unless @tools.key?(tool_name)
|
259
|
+
return {
|
260
|
+
jsonrpc: "2.0",
|
261
|
+
id: id,
|
262
|
+
error: {
|
263
|
+
code: -32602,
|
264
|
+
message: "Unknown tool",
|
265
|
+
data: "Tool '#{tool_name}' not found"
|
266
|
+
}
|
267
|
+
}
|
268
|
+
end
|
199
269
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
270
|
+
begin
|
271
|
+
result = call_tool(tool_name, arguments)
|
272
|
+
{
|
273
|
+
jsonrpc: "2.0",
|
274
|
+
id: id,
|
275
|
+
result: {
|
276
|
+
content: [
|
277
|
+
{
|
278
|
+
type: "text",
|
279
|
+
text: result
|
280
|
+
}
|
281
|
+
],
|
282
|
+
isError: false
|
283
|
+
}
|
208
284
|
}
|
209
|
-
|
285
|
+
rescue => e
|
286
|
+
File.write(Mcpeasy::Config.log_file_path("gcal", "error"), "#{Time.now}: Tool error: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
|
287
|
+
{
|
288
|
+
jsonrpc: "2.0",
|
289
|
+
id: id,
|
290
|
+
result: {
|
291
|
+
content: [
|
292
|
+
{
|
293
|
+
type: "text",
|
294
|
+
text: "❌ Error: #{e.message}"
|
295
|
+
}
|
296
|
+
],
|
297
|
+
isError: true
|
298
|
+
}
|
299
|
+
}
|
300
|
+
end
|
210
301
|
end
|
211
302
|
|
212
|
-
|
213
|
-
result = call_tool(tool_name, arguments)
|
303
|
+
def prompts_list_response(id, params)
|
214
304
|
{
|
215
305
|
jsonrpc: "2.0",
|
216
306
|
id: id,
|
217
307
|
result: {
|
218
|
-
|
219
|
-
{
|
220
|
-
type: "text",
|
221
|
-
text: result
|
222
|
-
}
|
223
|
-
],
|
224
|
-
isError: false
|
308
|
+
prompts: @prompts
|
225
309
|
}
|
226
310
|
}
|
227
|
-
|
228
|
-
|
311
|
+
end
|
312
|
+
|
313
|
+
def prompts_get_response(id, params)
|
314
|
+
prompt_name = params["name"]
|
315
|
+
prompt = @prompts.find { |p| p[:name] == prompt_name }
|
316
|
+
|
317
|
+
unless prompt
|
318
|
+
return {
|
319
|
+
jsonrpc: "2.0",
|
320
|
+
id: id,
|
321
|
+
error: {
|
322
|
+
code: -32602,
|
323
|
+
message: "Unknown prompt",
|
324
|
+
data: "Prompt '#{prompt_name}' not found"
|
325
|
+
}
|
326
|
+
}
|
327
|
+
end
|
328
|
+
|
329
|
+
# Generate messages based on the prompt
|
330
|
+
messages = case prompt_name
|
331
|
+
when "check_schedule"
|
332
|
+
period = params["arguments"]&.dig("period") || "today"
|
333
|
+
[
|
334
|
+
{
|
335
|
+
role: "user",
|
336
|
+
content: {
|
337
|
+
type: "text",
|
338
|
+
text: "Check my calendar schedule for #{period}"
|
339
|
+
}
|
340
|
+
}
|
341
|
+
]
|
342
|
+
when "find_meeting"
|
343
|
+
keywords = params["arguments"]&.dig("keywords") || ""
|
344
|
+
[
|
345
|
+
{
|
346
|
+
role: "user",
|
347
|
+
content: {
|
348
|
+
type: "text",
|
349
|
+
text: "Find meetings containing: #{keywords}"
|
350
|
+
}
|
351
|
+
}
|
352
|
+
]
|
353
|
+
when "check_availability"
|
354
|
+
datetime = params["arguments"]&.dig("datetime") || "today"
|
355
|
+
[
|
356
|
+
{
|
357
|
+
role: "user",
|
358
|
+
content: {
|
359
|
+
type: "text",
|
360
|
+
text: "Am I free at #{datetime}?"
|
361
|
+
}
|
362
|
+
}
|
363
|
+
]
|
364
|
+
when "weekly_overview"
|
365
|
+
[
|
366
|
+
{
|
367
|
+
role: "user",
|
368
|
+
content: {
|
369
|
+
type: "text",
|
370
|
+
text: "Give me an overview of my schedule for the upcoming week"
|
371
|
+
}
|
372
|
+
}
|
373
|
+
]
|
374
|
+
when "meeting_conflicts"
|
375
|
+
date = params["arguments"]&.dig("date") || "today"
|
376
|
+
[
|
377
|
+
{
|
378
|
+
role: "user",
|
379
|
+
content: {
|
380
|
+
type: "text",
|
381
|
+
text: "Check for any overlapping or back-to-back meetings on #{date}"
|
382
|
+
}
|
383
|
+
}
|
384
|
+
]
|
385
|
+
else
|
386
|
+
[]
|
387
|
+
end
|
388
|
+
|
229
389
|
{
|
230
390
|
jsonrpc: "2.0",
|
231
391
|
id: id,
|
232
392
|
result: {
|
233
|
-
|
234
|
-
|
235
|
-
type: "text",
|
236
|
-
text: "❌ Error: #{e.message}"
|
237
|
-
}
|
238
|
-
],
|
239
|
-
isError: true
|
393
|
+
description: prompt[:description],
|
394
|
+
messages: messages
|
240
395
|
}
|
241
396
|
}
|
242
397
|
end
|
243
|
-
end
|
244
398
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
399
|
+
def call_tool(tool_name, arguments)
|
400
|
+
case tool_name
|
401
|
+
when "test_connection"
|
402
|
+
test_connection
|
403
|
+
when "list_events"
|
404
|
+
list_events(arguments)
|
405
|
+
when "list_calendars"
|
406
|
+
list_calendars(arguments)
|
407
|
+
when "search_events"
|
408
|
+
search_events(arguments)
|
409
|
+
else
|
410
|
+
raise "Unknown tool: #{tool_name}"
|
411
|
+
end
|
257
412
|
end
|
258
|
-
end
|
259
413
|
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
414
|
+
def test_connection
|
415
|
+
tool = Service.new
|
416
|
+
response = tool.test_connection
|
417
|
+
if response[:ok]
|
418
|
+
"✅ Successfully connected to Google Calendar.\n" \
|
419
|
+
" User: #{response[:user]} (#{response[:email]})"
|
420
|
+
else
|
421
|
+
raise "Connection test failed"
|
422
|
+
end
|
268
423
|
end
|
269
|
-
end
|
270
424
|
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
425
|
+
def list_events(arguments)
|
426
|
+
start_date = arguments["start_date"]
|
427
|
+
end_date = arguments["end_date"]
|
428
|
+
max_results = arguments["max_results"]&.to_i || 20
|
429
|
+
calendar_id = arguments["calendar_id"] || "primary"
|
430
|
+
|
431
|
+
tool = Service.new
|
432
|
+
result = tool.list_events(
|
433
|
+
start_date: start_date,
|
434
|
+
end_date: end_date,
|
435
|
+
max_results: max_results,
|
436
|
+
calendar_id: calendar_id
|
437
|
+
)
|
438
|
+
events = result[:events]
|
439
|
+
|
440
|
+
if events.empty?
|
441
|
+
"📅 No events found for the specified date range"
|
442
|
+
else
|
443
|
+
output = "📅 Found #{result[:count]} event(s):\n\n"
|
444
|
+
events.each_with_index do |event, index|
|
445
|
+
output << "#{index + 1}. **#{event[:summary] || "No title"}**\n"
|
446
|
+
output << " - Start: #{format_datetime(event[:start])}\n"
|
447
|
+
output << " - End: #{format_datetime(event[:end])}\n"
|
448
|
+
output << " - Description: #{event[:description] || "No description"}\n" if event[:description]
|
449
|
+
output << " - Location: #{event[:location]}\n" if event[:location]
|
450
|
+
output << " - Attendees: #{event[:attendees].join(", ")}\n" if event[:attendees]&.any?
|
451
|
+
output << " - Link: #{event[:html_link]}\n\n"
|
452
|
+
end
|
453
|
+
output
|
298
454
|
end
|
299
|
-
output
|
300
455
|
end
|
301
|
-
end
|
302
456
|
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
457
|
+
def list_calendars(arguments)
|
458
|
+
tool = Service.new
|
459
|
+
result = tool.list_calendars
|
460
|
+
calendars = result[:calendars]
|
461
|
+
|
462
|
+
if calendars.empty?
|
463
|
+
"📋 No calendars found"
|
464
|
+
else
|
465
|
+
output = "📋 Found #{result[:count]} calendar(s):\n\n"
|
466
|
+
calendars.each_with_index do |calendar, index|
|
467
|
+
output << "#{index + 1}. **#{calendar[:summary]}**\n"
|
468
|
+
output << " - ID: `#{calendar[:id]}`\n"
|
469
|
+
output << " - Description: #{calendar[:description]}\n" if calendar[:description]
|
470
|
+
output << " - Time Zone: #{calendar[:time_zone]}\n"
|
471
|
+
output << " - Access Role: #{calendar[:access_role]}\n"
|
472
|
+
output << " - Primary: Yes\n" if calendar[:primary]
|
473
|
+
output << "\n"
|
474
|
+
end
|
475
|
+
output
|
320
476
|
end
|
321
|
-
output
|
322
477
|
end
|
323
|
-
end
|
324
478
|
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
479
|
+
def search_events(arguments)
|
480
|
+
unless arguments["query"]
|
481
|
+
raise "Missing required argument: query"
|
482
|
+
end
|
483
|
+
|
484
|
+
query = arguments["query"].to_s
|
485
|
+
start_date = arguments["start_date"]
|
486
|
+
end_date = arguments["end_date"]
|
487
|
+
max_results = arguments["max_results"]&.to_i || 10
|
329
488
|
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
output
|
353
|
-
output << " - Location: #{event[:location]}\n" if event[:location]
|
354
|
-
output << " - Calendar: #{event[:calendar_id]}\n"
|
355
|
-
output << " - Link: #{event[:html_link]}\n\n"
|
489
|
+
tool = Service.new
|
490
|
+
result = tool.search_events(
|
491
|
+
query,
|
492
|
+
start_date: start_date,
|
493
|
+
end_date: end_date,
|
494
|
+
max_results: max_results
|
495
|
+
)
|
496
|
+
events = result[:events]
|
497
|
+
|
498
|
+
if events.empty?
|
499
|
+
"🔍 No events found matching '#{query}'"
|
500
|
+
else
|
501
|
+
output = "🔍 Found #{result[:count]} event(s) matching '#{query}':\n\n"
|
502
|
+
events.each_with_index do |event, index|
|
503
|
+
output << "#{index + 1}. **#{event[:summary] || "No title"}**\n"
|
504
|
+
output << " - Start: #{format_datetime(event[:start])}\n"
|
505
|
+
output << " - End: #{format_datetime(event[:end])}\n"
|
506
|
+
output << " - Description: #{event[:description] || "No description"}\n" if event[:description]
|
507
|
+
output << " - Location: #{event[:location]}\n" if event[:location]
|
508
|
+
output << " - Calendar: #{event[:calendar_id]}\n"
|
509
|
+
output << " - Link: #{event[:html_link]}\n\n"
|
510
|
+
end
|
511
|
+
output
|
356
512
|
end
|
357
|
-
output
|
358
513
|
end
|
359
|
-
end
|
360
514
|
|
361
|
-
|
515
|
+
private
|
362
516
|
|
363
|
-
|
364
|
-
|
517
|
+
def format_datetime(datetime_info)
|
518
|
+
return "Unknown" unless datetime_info
|
365
519
|
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
520
|
+
if datetime_info[:date]
|
521
|
+
# All-day event
|
522
|
+
datetime_info[:date]
|
523
|
+
elsif datetime_info[:date_time]
|
524
|
+
# Specific time event
|
525
|
+
time = Time.parse(datetime_info[:date_time])
|
526
|
+
time.strftime("%Y-%m-%d %H:%M")
|
527
|
+
else
|
528
|
+
"Unknown"
|
529
|
+
end
|
375
530
|
end
|
376
531
|
end
|
377
532
|
end
|
378
533
|
|
379
534
|
if __FILE__ == $0
|
380
|
-
MCPServer.new.run
|
535
|
+
Gcal::MCPServer.new.run
|
381
536
|
end
|