mcpeasy 0.2.0 → 0.3.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.
@@ -0,0 +1,547 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "google/apis/gmail_v1"
5
+ require "googleauth"
6
+ require "signet/oauth_2/client"
7
+ require "fileutils"
8
+ require "json"
9
+ require "time"
10
+ require "mail"
11
+ require "html2text"
12
+ require_relative "../_google/auth_server"
13
+ require_relative "../../mcpeasy/config"
14
+
15
+ module Gmail
16
+ class Service
17
+ SCOPES = [
18
+ "https://www.googleapis.com/auth/gmail.readonly",
19
+ "https://www.googleapis.com/auth/gmail.send",
20
+ "https://www.googleapis.com/auth/gmail.modify"
21
+ ]
22
+ SCOPE = SCOPES.join(" ")
23
+
24
+ def initialize(skip_auth: false)
25
+ ensure_env!
26
+ @service = Google::Apis::GmailV1::GmailService.new
27
+ @service.authorization = authorize unless skip_auth
28
+ end
29
+
30
+ def list_emails(start_date: nil, end_date: nil, max_results: 20, sender: nil, subject: nil, labels: nil, read_status: nil)
31
+ query_parts = []
32
+
33
+ # Date filtering
34
+ if start_date
35
+ query_parts << "after:#{start_date}"
36
+ end
37
+ if end_date
38
+ query_parts << "before:#{end_date}"
39
+ end
40
+
41
+ # Sender filtering
42
+ if sender
43
+ query_parts << "from:#{sender}"
44
+ end
45
+
46
+ # Subject filtering
47
+ if subject
48
+ query_parts << "subject:\"#{subject}\""
49
+ end
50
+
51
+ # Labels filtering
52
+ if labels && !labels.empty?
53
+ if labels.is_a?(Array)
54
+ labels.each { |label| query_parts << "label:#{label}" }
55
+ else
56
+ query_parts << "label:#{labels}"
57
+ end
58
+ end
59
+
60
+ # Read/unread status
61
+ case read_status&.downcase
62
+ when "unread"
63
+ query_parts << "is:unread"
64
+ when "read"
65
+ query_parts << "is:read"
66
+ end
67
+
68
+ query = query_parts.join(" ")
69
+
70
+ results = @service.list_user_messages(
71
+ "me",
72
+ q: query.empty? ? nil : query,
73
+ max_results: max_results
74
+ )
75
+
76
+ emails = (results.messages || []).map do |message|
77
+ get_email_summary(message.id)
78
+ end.compact
79
+
80
+ {emails: emails, count: emails.length}
81
+ rescue Google::Apis::Error => e
82
+ raise "Gmail API Error: #{e.message}"
83
+ rescue => e
84
+ log_error("list_emails", e)
85
+ raise e
86
+ end
87
+
88
+ def search_emails(query, max_results: 10)
89
+ results = @service.list_user_messages(
90
+ "me",
91
+ q: query,
92
+ max_results: max_results
93
+ )
94
+
95
+ emails = (results.messages || []).map do |message|
96
+ get_email_summary(message.id)
97
+ end.compact
98
+
99
+ {emails: emails, count: emails.length}
100
+ rescue Google::Apis::Error => e
101
+ raise "Gmail API Error: #{e.message}"
102
+ rescue => e
103
+ log_error("search_emails", e)
104
+ raise e
105
+ end
106
+
107
+ def get_email_content(email_id)
108
+ message = @service.get_user_message("me", email_id)
109
+
110
+ # Extract email metadata
111
+ headers = extract_headers(message.payload)
112
+
113
+ # Extract email body
114
+ body_data = extract_body(message.payload)
115
+
116
+ {
117
+ id: message.id,
118
+ thread_id: message.thread_id,
119
+ subject: headers["Subject"],
120
+ from: headers["From"],
121
+ to: headers["To"],
122
+ cc: headers["Cc"],
123
+ bcc: headers["Bcc"],
124
+ date: headers["Date"],
125
+ body: body_data[:text],
126
+ body_html: body_data[:html],
127
+ snippet: message.snippet,
128
+ labels: message.label_ids || [],
129
+ attachments: extract_attachments(message.payload)
130
+ }
131
+ rescue Google::Apis::Error => e
132
+ raise "Gmail API Error: #{e.message}"
133
+ rescue => e
134
+ log_error("get_email_content", e)
135
+ raise e
136
+ end
137
+
138
+ def send_email(to:, subject:, body:, cc: nil, bcc: nil, reply_to: nil)
139
+ mail = Mail.new do
140
+ to to
141
+ cc cc if cc
142
+ bcc bcc if bcc
143
+ subject subject
144
+ body body
145
+ reply_to reply_to if reply_to
146
+ end
147
+
148
+ raw_message = mail.to_s
149
+ encoded_message = Base64.urlsafe_encode64(raw_message)
150
+
151
+ message_object = Google::Apis::GmailV1::Message.new(raw: encoded_message)
152
+ result = @service.send_user_message("me", message_object)
153
+
154
+ {
155
+ success: true,
156
+ message_id: result.id,
157
+ thread_id: result.thread_id
158
+ }
159
+ rescue Google::Apis::Error => e
160
+ raise "Gmail API Error: #{e.message}"
161
+ rescue => e
162
+ log_error("send_email", e)
163
+ raise e
164
+ end
165
+
166
+ def reply_to_email(email_id:, body:, include_quoted: true)
167
+ # Get the original message to extract reply information
168
+ original = @service.get_user_message("me", email_id)
169
+ headers = extract_headers(original.payload)
170
+
171
+ # Build reply headers
172
+ original_subject = headers["Subject"] || ""
173
+ reply_subject = original_subject.start_with?("Re:") ? original_subject : "Re: #{original_subject}"
174
+
175
+ reply_to = headers["Reply-To"] || headers["From"]
176
+ message_id = headers["Message-ID"]
177
+
178
+ # Prepare reply body
179
+ reply_body = body
180
+ if include_quoted
181
+ original_body = extract_body(original.payload)[:text]
182
+ date_str = headers["Date"]
183
+ from_str = headers["From"]
184
+
185
+ reply_body += "\n\n"
186
+ reply_body += "On #{date_str}, #{from_str} wrote:\n"
187
+ reply_body += original_body.split("\n").map { |line| "> #{line}" }.join("\n")
188
+ end
189
+
190
+ mail = Mail.new do
191
+ to reply_to
192
+ subject reply_subject
193
+ body reply_body
194
+ in_reply_to message_id if message_id
195
+ references message_id if message_id
196
+ end
197
+
198
+ raw_message = mail.to_s
199
+ encoded_message = Base64.urlsafe_encode64(raw_message)
200
+
201
+ message_object = Google::Apis::GmailV1::Message.new(
202
+ raw: encoded_message,
203
+ thread_id: original.thread_id
204
+ )
205
+
206
+ result = @service.send_user_message("me", message_object)
207
+
208
+ {
209
+ success: true,
210
+ message_id: result.id,
211
+ thread_id: result.thread_id
212
+ }
213
+ rescue Google::Apis::Error => e
214
+ raise "Gmail API Error: #{e.message}"
215
+ rescue => e
216
+ log_error("reply_to_email", e)
217
+ raise e
218
+ end
219
+
220
+ def mark_as_read(email_id)
221
+ modify_message(email_id, remove_label_ids: ["UNREAD"])
222
+ end
223
+
224
+ def mark_as_unread(email_id)
225
+ modify_message(email_id, add_label_ids: ["UNREAD"])
226
+ end
227
+
228
+ def add_label(email_id, label)
229
+ label_id = resolve_label_id(label)
230
+ modify_message(email_id, add_label_ids: [label_id])
231
+ end
232
+
233
+ def remove_label(email_id, label)
234
+ label_id = resolve_label_id(label)
235
+ modify_message(email_id, remove_label_ids: [label_id])
236
+ end
237
+
238
+ def archive_email(email_id)
239
+ modify_message(email_id, remove_label_ids: ["INBOX"])
240
+ end
241
+
242
+ def trash_email(email_id)
243
+ @service.trash_user_message("me", email_id)
244
+ {success: true}
245
+ rescue Google::Apis::Error => e
246
+ raise "Gmail API Error: #{e.message}"
247
+ rescue => e
248
+ log_error("trash_email", e)
249
+ raise e
250
+ end
251
+
252
+ def test_connection
253
+ profile = @service.get_user_profile("me")
254
+ {
255
+ ok: true,
256
+ email: profile.email_address,
257
+ messages_total: profile.messages_total,
258
+ threads_total: profile.threads_total
259
+ }
260
+ rescue Google::Apis::Error => e
261
+ raise "Gmail API Error: #{e.message}"
262
+ rescue => e
263
+ log_error("test_connection", e)
264
+ raise e
265
+ end
266
+
267
+ def authenticate
268
+ perform_auth_flow
269
+ {success: true}
270
+ rescue => e
271
+ {success: false, error: e.message}
272
+ end
273
+
274
+ def perform_auth_flow
275
+ client_id = Mcpeasy::Config.google_client_id
276
+ client_secret = Mcpeasy::Config.google_client_secret
277
+
278
+ unless client_id && client_secret
279
+ raise "Google credentials not found. Please save your credentials.json file using: mcpz config set_google_credentials <path_to_credentials.json>"
280
+ end
281
+
282
+ # Create credentials using OAuth2 flow with localhost redirect
283
+ redirect_uri = "http://localhost:8080"
284
+ client = Signet::OAuth2::Client.new(
285
+ client_id: client_id,
286
+ client_secret: client_secret,
287
+ scope: SCOPE,
288
+ redirect_uri: redirect_uri,
289
+ authorization_uri: "https://accounts.google.com/o/oauth2/auth",
290
+ token_credential_uri: "https://oauth2.googleapis.com/token"
291
+ )
292
+
293
+ # Generate authorization URL
294
+ url = client.authorization_uri.to_s
295
+
296
+ puts "DEBUG: Client ID: #{client_id[0..20]}..."
297
+ puts "DEBUG: Scope: #{SCOPE}"
298
+ puts "DEBUG: Redirect URI: #{redirect_uri}"
299
+ puts
300
+
301
+ # Start callback server to capture OAuth code
302
+ puts "Starting temporary web server to capture OAuth callback..."
303
+ puts "Opening authorization URL in your default browser..."
304
+ puts url
305
+ puts
306
+
307
+ # Automatically open URL in default browser on macOS/Unix
308
+ if system("which open > /dev/null 2>&1")
309
+ system("open", url)
310
+ else
311
+ puts "Could not automatically open browser. Please copy the URL above manually."
312
+ end
313
+ puts
314
+ puts "Waiting for OAuth callback... (will timeout in 60 seconds)"
315
+
316
+ # Wait for the authorization code with timeout
317
+ code = GoogleAuthServer.capture_auth_code
318
+
319
+ unless code
320
+ raise "Failed to receive authorization code. Please try again."
321
+ end
322
+
323
+ puts "✅ Authorization code received!"
324
+ client.code = code
325
+ client.fetch_access_token!
326
+
327
+ # Save credentials to config
328
+ credentials_data = {
329
+ client_id: client.client_id,
330
+ client_secret: client.client_secret,
331
+ scope: client.scope,
332
+ refresh_token: client.refresh_token,
333
+ access_token: client.access_token,
334
+ expires_at: client.expires_at
335
+ }
336
+
337
+ Mcpeasy::Config.save_google_token(credentials_data)
338
+ puts "✅ Authentication successful! Token saved to config"
339
+
340
+ client
341
+ rescue => e
342
+ log_error("perform_auth_flow", e)
343
+ raise "Authentication flow failed: #{e.message}"
344
+ end
345
+
346
+ private
347
+
348
+ def authorize
349
+ credentials_data = Mcpeasy::Config.google_token
350
+ unless credentials_data
351
+ raise <<~ERROR
352
+ Gmail authentication required!
353
+ Run the auth command first:
354
+ mcpz gmail auth
355
+ ERROR
356
+ end
357
+
358
+ client = Signet::OAuth2::Client.new(
359
+ client_id: credentials_data.client_id,
360
+ client_secret: credentials_data.client_secret,
361
+ scope: credentials_data.scope.respond_to?(:to_a) ? credentials_data.scope.to_a.join(" ") : credentials_data.scope.to_s,
362
+ refresh_token: credentials_data.refresh_token,
363
+ access_token: credentials_data.access_token,
364
+ token_credential_uri: "https://oauth2.googleapis.com/token"
365
+ )
366
+
367
+ # Check if token needs refresh
368
+ if credentials_data.expires_at
369
+ expires_at = if credentials_data.expires_at.is_a?(String)
370
+ Time.parse(credentials_data.expires_at)
371
+ else
372
+ Time.at(credentials_data.expires_at)
373
+ end
374
+
375
+ if Time.now >= expires_at
376
+ client.refresh!
377
+ # Update saved credentials with new access token
378
+ updated_data = {
379
+ client_id: credentials_data.client_id,
380
+ client_secret: credentials_data.client_secret,
381
+ scope: credentials_data.scope.respond_to?(:to_a) ? credentials_data.scope.to_a.join(" ") : credentials_data.scope.to_s,
382
+ refresh_token: credentials_data.refresh_token,
383
+ access_token: client.access_token,
384
+ expires_at: client.expires_at
385
+ }
386
+ Mcpeasy::Config.save_google_token(updated_data)
387
+ end
388
+ end
389
+
390
+ client
391
+ rescue JSON::ParserError
392
+ raise "Invalid token data. Please re-run: mcpz gmail auth"
393
+ rescue => e
394
+ log_error("authorize", e)
395
+ raise "Authentication failed: #{e.message}"
396
+ end
397
+
398
+ def ensure_env!
399
+ unless Mcpeasy::Config.google_client_id && Mcpeasy::Config.google_client_secret
400
+ raise <<~ERROR
401
+ Google API credentials not configured!
402
+ Please save your Google credentials.json file using:
403
+ mcpz config set_google_credentials <path_to_credentials.json>
404
+ ERROR
405
+ end
406
+ end
407
+
408
+ def get_email_summary(message_id)
409
+ message = @service.get_user_message("me", message_id, format: "metadata")
410
+ headers = extract_headers(message.payload)
411
+
412
+ {
413
+ id: message.id,
414
+ thread_id: message.thread_id,
415
+ subject: headers["Subject"],
416
+ from: headers["From"],
417
+ date: headers["Date"],
418
+ snippet: message.snippet,
419
+ labels: message.label_ids || []
420
+ }
421
+ rescue => e
422
+ log_error("get_email_summary", e)
423
+ nil
424
+ end
425
+
426
+ def extract_headers(payload)
427
+ headers = {}
428
+ (payload.headers || []).each do |header|
429
+ headers[header.name] = header.value
430
+ end
431
+ headers
432
+ end
433
+
434
+ def extract_body(payload)
435
+ body_data = {text: "", html: ""}
436
+
437
+ if payload.body&.data
438
+ # Simple message body
439
+ decoded_body = Base64.urlsafe_decode64(payload.body.data)
440
+ if payload.mime_type == "text/html"
441
+ body_data[:html] = decoded_body
442
+ body_data[:text] = Html2Text.convert(decoded_body)
443
+ else
444
+ body_data[:text] = decoded_body
445
+ end
446
+ elsif payload.parts
447
+ # Multipart message
448
+ extract_body_from_parts(payload.parts, body_data)
449
+ end
450
+
451
+ body_data
452
+ end
453
+
454
+ def extract_body_from_parts(parts, body_data)
455
+ parts.each do |part|
456
+ if part.mime_type == "text/plain" && part.body&.data
457
+ body_data[:text] += Base64.urlsafe_decode64(part.body.data)
458
+ elsif part.mime_type == "text/html" && part.body&.data
459
+ html_content = Base64.urlsafe_decode64(part.body.data)
460
+ body_data[:html] += html_content
461
+ # If we don't have plain text yet, convert from HTML
462
+ if body_data[:text].empty?
463
+ body_data[:text] = Html2Text.convert(html_content)
464
+ end
465
+ elsif part.parts
466
+ # Nested multipart
467
+ extract_body_from_parts(part.parts, body_data)
468
+ end
469
+ end
470
+ end
471
+
472
+ def extract_attachments(payload)
473
+ attachments = []
474
+
475
+ if payload.parts
476
+ extract_attachments_from_parts(payload.parts, attachments)
477
+ end
478
+
479
+ attachments
480
+ end
481
+
482
+ def extract_attachments_from_parts(parts, attachments)
483
+ parts.each do |part|
484
+ if part.filename && !part.filename.empty?
485
+ attachments << {
486
+ filename: part.filename,
487
+ mime_type: part.mime_type,
488
+ size: part.body&.size,
489
+ attachment_id: part.body&.attachment_id
490
+ }
491
+ elsif part.parts
492
+ extract_attachments_from_parts(part.parts, attachments)
493
+ end
494
+ end
495
+ end
496
+
497
+ def modify_message(email_id, add_label_ids: [], remove_label_ids: [])
498
+ request = Google::Apis::GmailV1::ModifyMessageRequest.new(
499
+ add_label_ids: add_label_ids,
500
+ remove_label_ids: remove_label_ids
501
+ )
502
+
503
+ @service.modify_message("me", email_id, request)
504
+ {success: true}
505
+ rescue Google::Apis::Error => e
506
+ raise "Gmail API Error: #{e.message}"
507
+ rescue => e
508
+ log_error("modify_message", e)
509
+ raise e
510
+ end
511
+
512
+ def resolve_label_id(label)
513
+ # Handle common label names
514
+ case label.upcase
515
+ when "INBOX"
516
+ "INBOX"
517
+ when "SENT"
518
+ "SENT"
519
+ when "DRAFT"
520
+ "DRAFT"
521
+ when "SPAM"
522
+ "SPAM"
523
+ when "TRASH"
524
+ "TRASH"
525
+ when "UNREAD"
526
+ "UNREAD"
527
+ when "STARRED"
528
+ "STARRED"
529
+ when "IMPORTANT"
530
+ "IMPORTANT"
531
+ else
532
+ # For custom labels, we'd need to fetch the labels list
533
+ # For now, return the label as-is and let the API handle it
534
+ label
535
+ end
536
+ end
537
+
538
+ def log_error(method, error)
539
+ Mcpeasy::Config.ensure_config_dirs
540
+ File.write(
541
+ Mcpeasy::Config.log_file_path("gmail", "error"),
542
+ "#{Time.now}: #{method} error: #{error.class}: #{error.message}\n#{error.backtrace.join("\n")}\n",
543
+ mode: "a"
544
+ )
545
+ end
546
+ end
547
+ end
@@ -22,7 +22,8 @@ module Gmeet
22
22
 
23
23
  def list_meetings(start_date: nil, end_date: nil, max_results: 20, calendar_id: "primary")
24
24
  # Default to today if no start date provided
25
- start_time = start_date ? Time.parse("#{start_date} 00:00:00") : Time.now.beginning_of_day
25
+ now = Time.now
26
+ start_time = start_date ? Time.parse("#{start_date} 00:00:00") : Time.new(now.year, now.month, now.day, 0, 0, 0)
26
27
  # Default to 7 days from start if no end date provided
27
28
  end_time = end_date ? Time.parse("#{end_date} 23:59:59") : start_time + 7 * 24 * 60 * 60
28
29
 
@@ -110,7 +111,8 @@ module Gmeet
110
111
 
111
112
  def search_meetings(query, start_date: nil, end_date: nil, max_results: 10)
112
113
  # Default to today if no start date provided
113
- start_time = start_date ? Time.parse("#{start_date} 00:00:00") : Time.now.beginning_of_day
114
+ now = Time.now
115
+ start_time = start_date ? Time.parse("#{start_date} 00:00:00") : Time.new(now.year, now.month, now.day, 0, 0, 0)
114
116
  # Default to 30 days from start if no end date provided
115
117
  end_time = end_date ? Time.parse("#{end_date} 23:59:59") : start_time + 30 * 24 * 60 * 60
116
118
 
data/mcpeasy.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ["joel@joelhelbling.com"]
10
10
 
11
11
  spec.summary = "MCP servers made easy"
12
- spec.description = "mcpeasy, LM squeezy - Easy-to-use MCP servers for Google Calendar, Google Drive, Google Meet, and Slack"
12
+ spec.description = "mcpeasy, LM squeezy - Easy-to-use MCP servers for Gmail, Google Calendar, Google Drive, Google Meet, and Slack"
13
13
  spec.homepage = "https://github.com/joelhelbling/mcpeasy"
14
14
  spec.license = "MIT"
15
15
  spec.required_ruby_version = ">= 3.0.0"
@@ -33,7 +33,12 @@ Gem::Specification.new do |spec|
33
33
  # Dependencies
34
34
  spec.add_dependency "google-apis-calendar_v3", "~> 0.35"
35
35
  spec.add_dependency "google-apis-drive_v3", "~> 0.45"
36
+ spec.add_dependency "google-apis-gmail_v1", "~> 0.40"
36
37
  spec.add_dependency "googleauth", "~> 1.8"
38
+ spec.add_dependency "html2text", "~> 0.2"
39
+ spec.add_dependency "mail", "~> 2.8"
40
+ spec.add_dependency "nokogiri", "~> 1.15"
41
+ spec.add_dependency "signet", "~> 0.19"
37
42
  spec.add_dependency "slack-ruby-client", "~> 2.1"
38
43
  spec.add_dependency "webrick", "~> 1.8"
39
44
  spec.add_dependency "thor", "~> 1.3"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mcpeasy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Helbling
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0.45'
40
+ - !ruby/object:Gem::Dependency
41
+ name: google-apis-gmail_v1
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.40'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.40'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: googleauth
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -51,6 +65,62 @@ dependencies:
51
65
  - - "~>"
52
66
  - !ruby/object:Gem::Version
53
67
  version: '1.8'
68
+ - !ruby/object:Gem::Dependency
69
+ name: html2text
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.2'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.2'
82
+ - !ruby/object:Gem::Dependency
83
+ name: mail
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.8'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.8'
96
+ - !ruby/object:Gem::Dependency
97
+ name: nokogiri
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '1.15'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '1.15'
110
+ - !ruby/object:Gem::Dependency
111
+ name: signet
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.19'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.19'
54
124
  - !ruby/object:Gem::Dependency
55
125
  name: slack-ruby-client
56
126
  requirement: !ruby/object:Gem::Requirement
@@ -121,8 +191,8 @@ dependencies:
121
191
  - - "~>"
122
192
  - !ruby/object:Gem::Version
123
193
  version: '1.50'
124
- description: mcpeasy, LM squeezy - Easy-to-use MCP servers for Google Calendar, Google
125
- Drive, Google Meet, and Slack
194
+ description: mcpeasy, LM squeezy - Easy-to-use MCP servers for Gmail, Google Calendar,
195
+ Google Drive, Google Meet, and Slack
126
196
  email:
127
197
  - joel@joelhelbling.com
128
198
  executables:
@@ -153,6 +223,10 @@ files:
153
223
  - lib/utilities/gdrive/cli.rb
154
224
  - lib/utilities/gdrive/mcp.rb
155
225
  - lib/utilities/gdrive/service.rb
226
+ - lib/utilities/gmail/README.md
227
+ - lib/utilities/gmail/cli.rb
228
+ - lib/utilities/gmail/mcp.rb
229
+ - lib/utilities/gmail/service.rb
156
230
  - lib/utilities/gmeet/README.md
157
231
  - lib/utilities/gmeet/cli.rb
158
232
  - lib/utilities/gmeet/mcp.rb