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.
@@ -3,379 +3,534 @@
3
3
 
4
4
  require "bundler/setup"
5
5
  require "json"
6
- require_relative "gcal_tool"
7
-
8
- class MCPServer
9
- def initialize
10
- @tools = {
11
- "test_connection" => {
12
- name: "test_connection",
13
- description: "Test the Google Calendar API connection",
14
- inputSchema: {
15
- type: "object",
16
- properties: {},
17
- required: []
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
- required: []
44
- }
45
- },
46
- "list_calendars" => {
47
- name: "list_calendars",
48
- description: "List available calendars",
49
- inputSchema: {
50
- type: "object",
51
- properties: {},
52
- required: []
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
- "search_events" => {
56
- name: "search_events",
57
- description: "Search for events by text content",
58
- inputSchema: {
59
- type: "object",
60
- properties: {
61
- query: {
62
- type: "string",
63
- description: "Search query to find events"
64
- },
65
- start_date: {
66
- type: "string",
67
- description: "Start date in YYYY-MM-DD format (default: today)"
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
- end_date: {
70
- type: "string",
71
- description: "End date in YYYY-MM-DD format (default: 30 days from start)"
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
- max_results: {
74
- type: "number",
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
- def run
85
- # Disable stdout buffering for immediate response
86
- $stdout.sync = true
137
+ def run
138
+ # Disable stdout buffering for immediate response
139
+ $stdout.sync = true
87
140
 
88
- # Log startup to file instead of stdout to avoid protocol interference
89
- Mcpeasy::Config.ensure_config_dirs
90
- File.write(Mcpeasy::Config.log_file_path("gcal", "startup"), "#{Time.now}: Google Calendar MCP Server starting on stdio\n", mode: "a")
91
- while (line = $stdin.gets)
92
- handle_request(line.strip)
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
- private
154
+ private
102
155
 
103
- def handle_request(line)
104
- return if line.empty?
156
+ def handle_request(line)
157
+ return if line.empty?
105
158
 
106
- begin
107
- request = JSON.parse(line)
108
- response = process_request(request)
109
- if response
110
- puts JSON.generate(response)
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
- end
113
- rescue JSON::ParserError => e
114
- error_response = {
115
- jsonrpc: "2.0",
116
- id: nil,
117
- error: {
118
- code: -32700,
119
- message: "Parse error",
120
- data: e.message
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
- puts JSON.generate(error_response)
124
- $stdout.flush
125
- rescue => e
126
- File.write(Mcpeasy::Config.log_file_path("gcal", "error"), "#{Time.now}: Error handling request: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
127
- error_response = {
128
- jsonrpc: "2.0",
129
- id: request&.dig("id"),
130
- error: {
131
- code: -32603,
132
- message: "Internal error",
133
- data: e.message
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
- def process_request(request)
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
- error: {
161
- code: -32601,
162
- message: "Method not found",
163
- data: "Unknown method: #{method}"
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
- def initialize_response(id, params)
170
- {
171
- jsonrpc: "2.0",
172
- id: id,
173
- result: {
174
- protocolVersion: "2024-11-05",
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
- def tools_list_response(id, params)
187
- {
188
- jsonrpc: "2.0",
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
- def tools_call_response(id, params)
197
- tool_name = params["name"]
198
- arguments = params["arguments"] || {}
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
- unless @tools.key?(tool_name)
201
- return {
202
- jsonrpc: "2.0",
203
- id: id,
204
- error: {
205
- code: -32602,
206
- message: "Unknown tool",
207
- data: "Tool '#{tool_name}' not found"
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
- begin
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
- content: [
219
- {
220
- type: "text",
221
- text: result
222
- }
223
- ],
224
- isError: false
308
+ prompts: @prompts
225
309
  }
226
310
  }
227
- rescue => e
228
- File.write(Mcpeasy::Config.log_file_path("gcal", "error"), "#{Time.now}: Tool error: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
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
- content: [
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
- def call_tool(tool_name, arguments)
246
- case tool_name
247
- when "test_connection"
248
- test_connection
249
- when "list_events"
250
- list_events(arguments)
251
- when "list_calendars"
252
- list_calendars(arguments)
253
- when "search_events"
254
- search_events(arguments)
255
- else
256
- raise "Unknown tool: #{tool_name}"
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
- def test_connection
261
- tool = GcalTool.new
262
- response = tool.test_connection
263
- if response[:ok]
264
- "✅ Successfully connected to Google Calendar.\n" \
265
- " User: #{response[:user]} (#{response[:email]})"
266
- else
267
- raise "Connection test failed"
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
- def list_events(arguments)
272
- start_date = arguments["start_date"]
273
- end_date = arguments["end_date"]
274
- max_results = arguments["max_results"]&.to_i || 20
275
- calendar_id = arguments["calendar_id"] || "primary"
276
-
277
- tool = GcalTool.new
278
- result = tool.list_events(
279
- start_date: start_date,
280
- end_date: end_date,
281
- max_results: max_results,
282
- calendar_id: calendar_id
283
- )
284
- events = result[:events]
285
-
286
- if events.empty?
287
- "📅 No events found for the specified date range"
288
- else
289
- output = "📅 Found #{result[:count]} event(s):\n\n"
290
- events.each_with_index do |event, index|
291
- output << "#{index + 1}. **#{event[:summary] || "No title"}**\n"
292
- output << " - Start: #{format_datetime(event[:start])}\n"
293
- output << " - End: #{format_datetime(event[:end])}\n"
294
- output << " - Description: #{event[:description] || "No description"}\n" if event[:description]
295
- output << " - Location: #{event[:location]}\n" if event[:location]
296
- output << " - Attendees: #{event[:attendees].join(", ")}\n" if event[:attendees]&.any?
297
- output << " - Link: #{event[:html_link]}\n\n"
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
- def list_calendars(arguments)
304
- tool = GcalTool.new
305
- result = tool.list_calendars
306
- calendars = result[:calendars]
307
-
308
- if calendars.empty?
309
- "📋 No calendars found"
310
- else
311
- output = "📋 Found #{result[:count]} calendar(s):\n\n"
312
- calendars.each_with_index do |calendar, index|
313
- output << "#{index + 1}. **#{calendar[:summary]}**\n"
314
- output << " - ID: `#{calendar[:id]}`\n"
315
- output << " - Description: #{calendar[:description]}\n" if calendar[:description]
316
- output << " - Time Zone: #{calendar[:time_zone]}\n"
317
- output << " - Access Role: #{calendar[:access_role]}\n"
318
- output << " - Primary: Yes\n" if calendar[:primary]
319
- output << "\n"
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
- def search_events(arguments)
326
- unless arguments["query"]
327
- raise "Missing required argument: query"
328
- end
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
- query = arguments["query"].to_s
331
- start_date = arguments["start_date"]
332
- end_date = arguments["end_date"]
333
- max_results = arguments["max_results"]&.to_i || 10
334
-
335
- tool = GcalTool.new
336
- result = tool.search_events(
337
- query,
338
- start_date: start_date,
339
- end_date: end_date,
340
- max_results: max_results
341
- )
342
- events = result[:events]
343
-
344
- if events.empty?
345
- "🔍 No events found matching '#{query}'"
346
- else
347
- output = "🔍 Found #{result[:count]} event(s) matching '#{query}':\n\n"
348
- events.each_with_index do |event, index|
349
- output << "#{index + 1}. **#{event[:summary] || "No title"}**\n"
350
- output << " - Start: #{format_datetime(event[:start])}\n"
351
- output << " - End: #{format_datetime(event[:end])}\n"
352
- output << " - Description: #{event[:description] || "No description"}\n" if event[:description]
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
- private
515
+ private
362
516
 
363
- def format_datetime(datetime_info)
364
- return "Unknown" unless datetime_info
517
+ def format_datetime(datetime_info)
518
+ return "Unknown" unless datetime_info
365
519
 
366
- if datetime_info[:date]
367
- # All-day event
368
- datetime_info[:date]
369
- elsif datetime_info[:date_time]
370
- # Specific time event
371
- time = Time.parse(datetime_info[:date_time])
372
- time.strftime("%Y-%m-%d %H:%M")
373
- else
374
- "Unknown"
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