mcpeasy 0.1.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 +7 -0
- data/.claude/settings.json +15 -0
- data/.claudeignore +4 -0
- data/.envrc +2 -0
- data/.mcp.json +40 -0
- data/CLAUDE.md +170 -0
- data/README.md +161 -0
- data/bin/mcpz +6 -0
- data/env.template +11 -0
- data/ext/setup.rb +7 -0
- data/lib/mcpeasy/cli.rb +154 -0
- data/lib/mcpeasy/config.rb +102 -0
- data/lib/mcpeasy/setup.rb +22 -0
- data/lib/mcpeasy/version.rb +5 -0
- data/lib/mcpeasy.rb +9 -0
- data/lib/utilities/_google/auth_server.rb +149 -0
- data/lib/utilities/gcal/README.md +237 -0
- data/lib/utilities/gcal/cli.rb +134 -0
- data/lib/utilities/gcal/gcal_tool.rb +308 -0
- data/lib/utilities/gcal/mcp.rb +381 -0
- data/lib/utilities/gdrive/README.md +269 -0
- data/lib/utilities/gdrive/cli.rb +118 -0
- data/lib/utilities/gdrive/gdrive_tool.rb +291 -0
- data/lib/utilities/gdrive/mcp.rb +347 -0
- data/lib/utilities/gmeet/README.md +133 -0
- data/lib/utilities/gmeet/cli.rb +157 -0
- data/lib/utilities/gmeet/gmeet_tool.rb +407 -0
- data/lib/utilities/gmeet/mcp.rb +438 -0
- data/lib/utilities/slack/README.md +211 -0
- data/lib/utilities/slack/cli.rb +74 -0
- data/lib/utilities/slack/mcp.rb +280 -0
- data/lib/utilities/slack/slack_tool.rb +119 -0
- data/logs/.keep +0 -0
- data/mcpeasy.gemspec +47 -0
- metadata +191 -0
@@ -0,0 +1,308 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "google/apis/calendar_v3"
|
5
|
+
require "googleauth"
|
6
|
+
require "signet/oauth_2/client"
|
7
|
+
require "fileutils"
|
8
|
+
require "json"
|
9
|
+
require "time"
|
10
|
+
require_relative "../_google/auth_server"
|
11
|
+
require_relative "../../mcpeasy/config"
|
12
|
+
|
13
|
+
class GcalTool
|
14
|
+
SCOPES = [
|
15
|
+
"https://www.googleapis.com/auth/calendar.readonly",
|
16
|
+
"https://www.googleapis.com/auth/drive.readonly"
|
17
|
+
]
|
18
|
+
SCOPE = SCOPES.join(" ")
|
19
|
+
|
20
|
+
def initialize(skip_auth: false)
|
21
|
+
ensure_env!
|
22
|
+
@service = Google::Apis::CalendarV3::CalendarService.new
|
23
|
+
@service.authorization = authorize unless skip_auth
|
24
|
+
end
|
25
|
+
|
26
|
+
def list_events(start_date: nil, end_date: nil, max_results: 20, calendar_id: "primary")
|
27
|
+
# Default to today if no start date provided
|
28
|
+
start_time = start_date ? Time.parse("#{start_date} 00:00:00") : Time.now.beginning_of_day
|
29
|
+
# Default to 7 days from start if no end date provided
|
30
|
+
end_time = end_date ? Time.parse("#{end_date} 23:59:59") : start_time + 7 * 24 * 60 * 60
|
31
|
+
|
32
|
+
results = @service.list_events(
|
33
|
+
calendar_id,
|
34
|
+
max_results: max_results,
|
35
|
+
single_events: true,
|
36
|
+
order_by: "startTime",
|
37
|
+
time_min: start_time.utc.iso8601,
|
38
|
+
time_max: end_time.utc.iso8601
|
39
|
+
)
|
40
|
+
|
41
|
+
events = results.items.map do |event|
|
42
|
+
{
|
43
|
+
id: event.id,
|
44
|
+
summary: event.summary,
|
45
|
+
description: event.description,
|
46
|
+
location: event.location,
|
47
|
+
start: format_event_time(event.start),
|
48
|
+
end: format_event_time(event.end),
|
49
|
+
attendees: format_attendees(event.attendees),
|
50
|
+
html_link: event.html_link,
|
51
|
+
calendar_id: calendar_id
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
{events: events, count: events.length}
|
56
|
+
rescue Google::Apis::Error => e
|
57
|
+
raise "Google Calendar API Error: #{e.message}"
|
58
|
+
rescue => e
|
59
|
+
log_error("list_events", e)
|
60
|
+
raise e
|
61
|
+
end
|
62
|
+
|
63
|
+
def list_calendars
|
64
|
+
results = @service.list_calendar_lists
|
65
|
+
|
66
|
+
calendars = results.items.map do |calendar|
|
67
|
+
{
|
68
|
+
id: calendar.id,
|
69
|
+
summary: calendar.summary,
|
70
|
+
description: calendar.description,
|
71
|
+
time_zone: calendar.time_zone,
|
72
|
+
access_role: calendar.access_role,
|
73
|
+
primary: calendar.primary
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
{calendars: calendars, count: calendars.length}
|
78
|
+
rescue Google::Apis::Error => e
|
79
|
+
raise "Google Calendar API Error: #{e.message}"
|
80
|
+
rescue => e
|
81
|
+
log_error("list_calendars", e)
|
82
|
+
raise e
|
83
|
+
end
|
84
|
+
|
85
|
+
def search_events(query, start_date: nil, end_date: nil, max_results: 10)
|
86
|
+
# Default to today if no start date provided
|
87
|
+
start_time = start_date ? Time.parse("#{start_date} 00:00:00") : Time.now.beginning_of_day
|
88
|
+
# Default to 30 days from start if no end date provided
|
89
|
+
end_time = end_date ? Time.parse("#{end_date} 23:59:59") : start_time + 30 * 24 * 60 * 60
|
90
|
+
|
91
|
+
results = @service.list_events(
|
92
|
+
"primary",
|
93
|
+
q: query,
|
94
|
+
max_results: max_results,
|
95
|
+
single_events: true,
|
96
|
+
order_by: "startTime",
|
97
|
+
time_min: start_time.utc.iso8601,
|
98
|
+
time_max: end_time.utc.iso8601
|
99
|
+
)
|
100
|
+
|
101
|
+
events = results.items.map do |event|
|
102
|
+
{
|
103
|
+
id: event.id,
|
104
|
+
summary: event.summary,
|
105
|
+
description: event.description,
|
106
|
+
location: event.location,
|
107
|
+
start: format_event_time(event.start),
|
108
|
+
end: format_event_time(event.end),
|
109
|
+
attendees: format_attendees(event.attendees),
|
110
|
+
html_link: event.html_link,
|
111
|
+
calendar_id: "primary"
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
{events: events, count: events.length}
|
116
|
+
rescue Google::Apis::Error => e
|
117
|
+
raise "Google Calendar API Error: #{e.message}"
|
118
|
+
rescue => e
|
119
|
+
log_error("search_events", e)
|
120
|
+
raise e
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_connection
|
124
|
+
calendar = @service.get_calendar("primary")
|
125
|
+
{
|
126
|
+
ok: true,
|
127
|
+
user: calendar.summary,
|
128
|
+
email: calendar.id
|
129
|
+
}
|
130
|
+
rescue Google::Apis::Error => e
|
131
|
+
raise "Google Calendar API Error: #{e.message}"
|
132
|
+
rescue => e
|
133
|
+
log_error("test_connection", e)
|
134
|
+
raise e
|
135
|
+
end
|
136
|
+
|
137
|
+
def authenticate
|
138
|
+
perform_auth_flow
|
139
|
+
{success: true}
|
140
|
+
rescue => e
|
141
|
+
{success: false, error: e.message}
|
142
|
+
end
|
143
|
+
|
144
|
+
def perform_auth_flow
|
145
|
+
client_id = Mcpeasy::Config.google_client_id
|
146
|
+
client_secret = Mcpeasy::Config.google_client_secret
|
147
|
+
|
148
|
+
unless client_id && client_secret
|
149
|
+
raise "Google credentials not found. Please save your credentials.json file using: mcpz config set_google_credentials <path_to_credentials.json>"
|
150
|
+
end
|
151
|
+
|
152
|
+
# Create credentials using OAuth2 flow with localhost redirect
|
153
|
+
redirect_uri = "http://localhost:8080"
|
154
|
+
client = Signet::OAuth2::Client.new(
|
155
|
+
client_id: client_id,
|
156
|
+
client_secret: client_secret,
|
157
|
+
scope: SCOPE,
|
158
|
+
redirect_uri: redirect_uri,
|
159
|
+
authorization_uri: "https://accounts.google.com/o/oauth2/auth",
|
160
|
+
token_credential_uri: "https://oauth2.googleapis.com/token"
|
161
|
+
)
|
162
|
+
|
163
|
+
# Generate authorization URL
|
164
|
+
url = client.authorization_uri.to_s
|
165
|
+
|
166
|
+
puts "DEBUG: Client ID: #{client_id[0..20]}..."
|
167
|
+
puts "DEBUG: Scope: #{SCOPE}"
|
168
|
+
puts "DEBUG: Redirect URI: #{redirect_uri}"
|
169
|
+
puts
|
170
|
+
|
171
|
+
# Start callback server to capture OAuth code
|
172
|
+
puts "Starting temporary web server to capture OAuth callback..."
|
173
|
+
puts "Opening authorization URL in your default browser..."
|
174
|
+
puts url
|
175
|
+
puts
|
176
|
+
|
177
|
+
# Automatically open URL in default browser on macOS/Unix
|
178
|
+
if system("which open > /dev/null 2>&1")
|
179
|
+
system("open", url)
|
180
|
+
else
|
181
|
+
puts "Could not automatically open browser. Please copy the URL above manually."
|
182
|
+
end
|
183
|
+
puts
|
184
|
+
puts "Waiting for OAuth callback... (will timeout in 60 seconds)"
|
185
|
+
|
186
|
+
# Wait for the authorization code with timeout
|
187
|
+
code = GoogleAuthServer.capture_auth_code
|
188
|
+
|
189
|
+
unless code
|
190
|
+
raise "Failed to receive authorization code. Please try again."
|
191
|
+
end
|
192
|
+
|
193
|
+
puts "✅ Authorization code received!"
|
194
|
+
client.code = code
|
195
|
+
client.fetch_access_token!
|
196
|
+
|
197
|
+
# Save credentials to config
|
198
|
+
credentials_data = {
|
199
|
+
client_id: client.client_id,
|
200
|
+
client_secret: client.client_secret,
|
201
|
+
scope: client.scope,
|
202
|
+
refresh_token: client.refresh_token,
|
203
|
+
access_token: client.access_token,
|
204
|
+
expires_at: client.expires_at
|
205
|
+
}
|
206
|
+
|
207
|
+
Mcpeasy::Config.save_google_token(credentials_data)
|
208
|
+
puts "✅ Authentication successful! Token saved to config"
|
209
|
+
|
210
|
+
client
|
211
|
+
rescue => e
|
212
|
+
log_error("perform_auth_flow", e)
|
213
|
+
raise "Authentication flow failed: #{e.message}"
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
def authorize
|
219
|
+
credentials_data = Mcpeasy::Config.google_token
|
220
|
+
unless credentials_data
|
221
|
+
raise <<~ERROR
|
222
|
+
Google Calendar authentication required!
|
223
|
+
Run the auth command first:
|
224
|
+
mcpz gcal auth
|
225
|
+
ERROR
|
226
|
+
end
|
227
|
+
|
228
|
+
client = Signet::OAuth2::Client.new(
|
229
|
+
client_id: credentials_data.client_id,
|
230
|
+
client_secret: credentials_data.client_secret,
|
231
|
+
scope: credentials_data.scope.respond_to?(:to_a) ? credentials_data.scope.to_a.join(" ") : credentials_data.scope.to_s,
|
232
|
+
refresh_token: credentials_data.refresh_token,
|
233
|
+
access_token: credentials_data.access_token,
|
234
|
+
token_credential_uri: "https://oauth2.googleapis.com/token"
|
235
|
+
)
|
236
|
+
|
237
|
+
# Check if token needs refresh
|
238
|
+
if credentials_data.expires_at
|
239
|
+
expires_at = if credentials_data.expires_at.is_a?(String)
|
240
|
+
Time.parse(credentials_data.expires_at)
|
241
|
+
else
|
242
|
+
Time.at(credentials_data.expires_at)
|
243
|
+
end
|
244
|
+
|
245
|
+
if Time.now >= expires_at
|
246
|
+
client.refresh!
|
247
|
+
# Update saved credentials with new access token
|
248
|
+
updated_data = {
|
249
|
+
client_id: credentials_data.client_id,
|
250
|
+
client_secret: credentials_data.client_secret,
|
251
|
+
scope: credentials_data.scope.respond_to?(:to_a) ? credentials_data.scope.to_a.join(" ") : credentials_data.scope.to_s,
|
252
|
+
refresh_token: credentials_data.refresh_token,
|
253
|
+
access_token: client.access_token,
|
254
|
+
expires_at: client.expires_at
|
255
|
+
}
|
256
|
+
Mcpeasy::Config.save_google_token(updated_data)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
client
|
261
|
+
rescue JSON::ParserError
|
262
|
+
raise "Invalid token data. Please re-run: mcpz gcal auth"
|
263
|
+
rescue => e
|
264
|
+
log_error("authorize", e)
|
265
|
+
raise "Authentication failed: #{e.message}"
|
266
|
+
end
|
267
|
+
|
268
|
+
def ensure_env!
|
269
|
+
unless Mcpeasy::Config.google_client_id && Mcpeasy::Config.google_client_secret
|
270
|
+
raise <<~ERROR
|
271
|
+
Google API credentials not configured!
|
272
|
+
Please save your Google credentials.json file using:
|
273
|
+
mcpz config set_google_credentials <path_to_credentials.json>
|
274
|
+
ERROR
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def format_event_time(event_time)
|
279
|
+
return nil unless event_time
|
280
|
+
|
281
|
+
if event_time.date
|
282
|
+
# All-day event
|
283
|
+
{date: event_time.date}
|
284
|
+
elsif event_time.date_time
|
285
|
+
# Specific time event
|
286
|
+
{date_time: event_time.date_time.iso8601}
|
287
|
+
else
|
288
|
+
nil
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def format_attendees(attendees)
|
293
|
+
return [] unless attendees
|
294
|
+
|
295
|
+
attendees.map do |attendee|
|
296
|
+
attendee.email
|
297
|
+
end.compact
|
298
|
+
end
|
299
|
+
|
300
|
+
def log_error(method, error)
|
301
|
+
Mcpeasy::Config.ensure_config_dirs
|
302
|
+
File.write(
|
303
|
+
Mcpeasy::Config.log_file_path("gcal", "error"),
|
304
|
+
"#{Time.now}: #{method} error: #{error.class}: #{error.message}\n#{error.backtrace.join("\n")}\n",
|
305
|
+
mode: "a"
|
306
|
+
)
|
307
|
+
end
|
308
|
+
end
|
@@ -0,0 +1,381 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
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)"
|
41
|
+
}
|
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: []
|
53
|
+
}
|
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)"
|
68
|
+
},
|
69
|
+
end_date: {
|
70
|
+
type: "string",
|
71
|
+
description: "End date in YYYY-MM-DD format (default: 30 days from start)"
|
72
|
+
},
|
73
|
+
max_results: {
|
74
|
+
type: "number",
|
75
|
+
description: "Maximum number of events to return (default: 10)"
|
76
|
+
}
|
77
|
+
},
|
78
|
+
required: ["query"]
|
79
|
+
}
|
80
|
+
}
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def run
|
85
|
+
# Disable stdout buffering for immediate response
|
86
|
+
$stdout.sync = true
|
87
|
+
|
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)
|
93
|
+
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
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def handle_request(line)
|
104
|
+
return if line.empty?
|
105
|
+
|
106
|
+
begin
|
107
|
+
request = JSON.parse(line)
|
108
|
+
response = process_request(request)
|
109
|
+
if response
|
110
|
+
puts JSON.generate(response)
|
111
|
+
$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
|
121
|
+
}
|
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
|
134
|
+
}
|
135
|
+
}
|
136
|
+
puts JSON.generate(error_response)
|
137
|
+
$stdout.flush
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
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
|
157
|
+
{
|
158
|
+
jsonrpc: "2.0",
|
159
|
+
id: id,
|
160
|
+
error: {
|
161
|
+
code: -32601,
|
162
|
+
message: "Method not found",
|
163
|
+
data: "Unknown method: #{method}"
|
164
|
+
}
|
165
|
+
}
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
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"
|
181
|
+
}
|
182
|
+
}
|
183
|
+
}
|
184
|
+
end
|
185
|
+
|
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
|
195
|
+
|
196
|
+
def tools_call_response(id, params)
|
197
|
+
tool_name = params["name"]
|
198
|
+
arguments = params["arguments"] || {}
|
199
|
+
|
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"
|
208
|
+
}
|
209
|
+
}
|
210
|
+
end
|
211
|
+
|
212
|
+
begin
|
213
|
+
result = call_tool(tool_name, arguments)
|
214
|
+
{
|
215
|
+
jsonrpc: "2.0",
|
216
|
+
id: id,
|
217
|
+
result: {
|
218
|
+
content: [
|
219
|
+
{
|
220
|
+
type: "text",
|
221
|
+
text: result
|
222
|
+
}
|
223
|
+
],
|
224
|
+
isError: false
|
225
|
+
}
|
226
|
+
}
|
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")
|
229
|
+
{
|
230
|
+
jsonrpc: "2.0",
|
231
|
+
id: id,
|
232
|
+
result: {
|
233
|
+
content: [
|
234
|
+
{
|
235
|
+
type: "text",
|
236
|
+
text: "❌ Error: #{e.message}"
|
237
|
+
}
|
238
|
+
],
|
239
|
+
isError: true
|
240
|
+
}
|
241
|
+
}
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
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}"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
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"
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
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"
|
298
|
+
end
|
299
|
+
output
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
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"
|
320
|
+
end
|
321
|
+
output
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def search_events(arguments)
|
326
|
+
unless arguments["query"]
|
327
|
+
raise "Missing required argument: query"
|
328
|
+
end
|
329
|
+
|
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"
|
356
|
+
end
|
357
|
+
output
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
private
|
362
|
+
|
363
|
+
def format_datetime(datetime_info)
|
364
|
+
return "Unknown" unless datetime_info
|
365
|
+
|
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"
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
if __FILE__ == $0
|
380
|
+
MCPServer.new.run
|
381
|
+
end
|