mcpeasy 0.1.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.claudeignore +0 -3
  3. data/.mcp.json +19 -1
  4. data/CHANGELOG.md +59 -0
  5. data/CLAUDE.md +19 -5
  6. data/README.md +19 -3
  7. data/lib/mcpeasy/cli.rb +62 -10
  8. data/lib/mcpeasy/config.rb +22 -1
  9. data/lib/mcpeasy/setup.rb +1 -0
  10. data/lib/mcpeasy/version.rb +1 -1
  11. data/lib/utilities/gcal/README.md +11 -3
  12. data/lib/utilities/gcal/cli.rb +110 -108
  13. data/lib/utilities/gcal/mcp.rb +463 -308
  14. data/lib/utilities/gcal/service.rb +312 -0
  15. data/lib/utilities/gdrive/README.md +3 -3
  16. data/lib/utilities/gdrive/cli.rb +98 -96
  17. data/lib/utilities/gdrive/mcp.rb +290 -288
  18. data/lib/utilities/gdrive/service.rb +293 -0
  19. data/lib/utilities/gmail/README.md +278 -0
  20. data/lib/utilities/gmail/cli.rb +264 -0
  21. data/lib/utilities/gmail/mcp.rb +846 -0
  22. data/lib/utilities/gmail/service.rb +547 -0
  23. data/lib/utilities/gmeet/cli.rb +131 -129
  24. data/lib/utilities/gmeet/mcp.rb +374 -372
  25. data/lib/utilities/gmeet/service.rb +411 -0
  26. data/lib/utilities/notion/README.md +287 -0
  27. data/lib/utilities/notion/cli.rb +245 -0
  28. data/lib/utilities/notion/mcp.rb +607 -0
  29. data/lib/utilities/notion/service.rb +327 -0
  30. data/lib/utilities/slack/README.md +3 -3
  31. data/lib/utilities/slack/cli.rb +69 -54
  32. data/lib/utilities/slack/mcp.rb +277 -226
  33. data/lib/utilities/slack/service.rb +134 -0
  34. data/mcpeasy.gemspec +6 -1
  35. metadata +87 -10
  36. data/env.template +0 -11
  37. data/lib/utilities/gcal/gcal_tool.rb +0 -308
  38. data/lib/utilities/gdrive/gdrive_tool.rb +0 -291
  39. data/lib/utilities/gmeet/gmeet_tool.rb +0 -407
  40. data/lib/utilities/slack/slack_tool.rb +0 -119
  41. data/logs/.keep +0 -0
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "slack-ruby-client"
5
+ require "mcpeasy/config"
6
+
7
+ module Slack
8
+ class Service
9
+ def initialize
10
+ ensure_env!
11
+ @client = ::Slack::Web::Client.new(
12
+ token: Mcpeasy::Config.slack_bot_token,
13
+ timeout: 10, # 10 second timeout
14
+ open_timeout: 5 # 5 second connection timeout
15
+ )
16
+ end
17
+
18
+ def post_message(channel:, text:, username: nil, thread_ts: nil)
19
+ # Clean up parameters
20
+ clean_channel = channel.to_s.sub(/^#/, "").strip
21
+ clean_text = text.to_s.strip
22
+
23
+ # Validate inputs
24
+ raise "Channel cannot be empty" if clean_channel.empty?
25
+ raise "Text cannot be empty" if clean_text.empty?
26
+
27
+ # Build request parameters
28
+ params = {
29
+ channel: clean_channel,
30
+ text: clean_text
31
+ }
32
+ params[:username] = username if username && !username.to_s.strip.empty?
33
+ params[:thread_ts] = thread_ts if thread_ts && !thread_ts.to_s.strip.empty?
34
+
35
+ # Retry logic for reliability
36
+ max_retries = 3
37
+ retry_count = 0
38
+
39
+ begin
40
+ response = @client.chat_postMessage(params)
41
+
42
+ if response["ok"]
43
+ response
44
+ else
45
+ raise "Failed to post message: #{response["error"]} (#{response.inspect})"
46
+ end
47
+ rescue ::Slack::Web::Api::Errors::TooManyRequestsError => e
48
+ retry_count += 1
49
+ if retry_count <= max_retries
50
+ sleep_time = e.retry_after || 1
51
+ sleep(sleep_time)
52
+ retry
53
+ else
54
+ raise "Slack API Error: #{e.message}"
55
+ end
56
+ rescue ::Slack::Web::Api::Errors::SlackError => e
57
+ retry_count += 1
58
+ if retry_count <= max_retries && retryable_error?(e)
59
+ sleep(0.5 * retry_count) # Exponential backoff
60
+ retry
61
+ else
62
+ raise "Slack API Error: #{e.message}"
63
+ end
64
+ rescue => e
65
+ Mcpeasy::Config.ensure_config_dirs
66
+ File.write(Mcpeasy::Config.log_file_path("slack", "error"), "#{Time.now}: Slack::Service error: #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
67
+ raise e
68
+ end
69
+ end
70
+
71
+ def list_channels(limit: 100, cursor: nil, exclude_archived: true)
72
+ params = {
73
+ types: "public_channel,private_channel",
74
+ limit: [limit.to_i, 1000].min,
75
+ exclude_archived: exclude_archived
76
+ }
77
+ params[:cursor] = cursor if cursor && !cursor.to_s.strip.empty?
78
+
79
+ response = @client.conversations_list(params)
80
+
81
+ if response["ok"]
82
+ channels = response["channels"].to_a.map do |channel|
83
+ {
84
+ name: channel["name"],
85
+ id: channel["id"]
86
+ }
87
+ end
88
+
89
+ {
90
+ channels: channels,
91
+ has_more: response.dig("response_metadata", "next_cursor") ? true : false,
92
+ next_cursor: response.dig("response_metadata", "next_cursor")
93
+ }
94
+ else
95
+ raise "Failed to list channels: #{response["error"]}"
96
+ end
97
+ rescue ::Slack::Web::Api::Errors::SlackError => e
98
+ raise "Slack API Error: #{e.message}"
99
+ end
100
+
101
+ def test_connection
102
+ response = @client.auth_test
103
+
104
+ if response["ok"]
105
+ response
106
+ else
107
+ raise "Authentication failed: #{response["error"]}"
108
+ end
109
+ rescue ::Slack::Web::Api::Errors::SlackError => e
110
+ raise "Slack API Error: #{e.message}"
111
+ end
112
+
113
+ def tool_definitions
114
+ end
115
+
116
+ private
117
+
118
+ def retryable_error?(error)
119
+ # Network-related errors that might be temporary
120
+ error.is_a?(::Slack::Web::Api::Errors::TimeoutError) ||
121
+ error.is_a?(::Slack::Web::Api::Errors::UnavailableError) ||
122
+ (error.respond_to?(:message) && error.message.include?("timeout"))
123
+ end
124
+
125
+ def ensure_env!
126
+ unless Mcpeasy::Config.slack_bot_token
127
+ raise <<~ERROR
128
+ Slack bot token is not configured!
129
+ Please run: mcp set slack_bot_token YOUR_TOKEN
130
+ ERROR
131
+ end
132
+ end
133
+ end
134
+ end
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.1.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:
@@ -134,10 +204,10 @@ files:
134
204
  - ".claudeignore"
135
205
  - ".envrc"
136
206
  - ".mcp.json"
207
+ - CHANGELOG.md
137
208
  - CLAUDE.md
138
209
  - README.md
139
210
  - bin/mcpz
140
- - env.template
141
211
  - ext/setup.rb
142
212
  - lib/mcpeasy.rb
143
213
  - lib/mcpeasy/cli.rb
@@ -147,21 +217,28 @@ files:
147
217
  - lib/utilities/_google/auth_server.rb
148
218
  - lib/utilities/gcal/README.md
149
219
  - lib/utilities/gcal/cli.rb
150
- - lib/utilities/gcal/gcal_tool.rb
151
220
  - lib/utilities/gcal/mcp.rb
221
+ - lib/utilities/gcal/service.rb
152
222
  - lib/utilities/gdrive/README.md
153
223
  - lib/utilities/gdrive/cli.rb
154
- - lib/utilities/gdrive/gdrive_tool.rb
155
224
  - lib/utilities/gdrive/mcp.rb
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
- - lib/utilities/gmeet/gmeet_tool.rb
159
232
  - lib/utilities/gmeet/mcp.rb
233
+ - lib/utilities/gmeet/service.rb
234
+ - lib/utilities/notion/README.md
235
+ - lib/utilities/notion/cli.rb
236
+ - lib/utilities/notion/mcp.rb
237
+ - lib/utilities/notion/service.rb
160
238
  - lib/utilities/slack/README.md
161
239
  - lib/utilities/slack/cli.rb
162
240
  - lib/utilities/slack/mcp.rb
163
- - lib/utilities/slack/slack_tool.rb
164
- - logs/.keep
241
+ - lib/utilities/slack/service.rb
165
242
  - mcpeasy.gemspec
166
243
  homepage: https://github.com/joelhelbling/mcpeasy
167
244
  licenses:
@@ -185,7 +262,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
185
262
  - !ruby/object:Gem::Version
186
263
  version: '0'
187
264
  requirements: []
188
- rubygems_version: 3.6.7
265
+ rubygems_version: 3.6.9
189
266
  specification_version: 4
190
267
  summary: MCP servers made easy
191
268
  test_files: []
data/env.template DELETED
@@ -1,11 +0,0 @@
1
- # Copy this file to .env and replace the placeholders with your own values
2
-
3
- # Used by the slack_poster.rb script
4
- SLACK_BOT_TOKEN=xoxb-REPLACE-ME
5
-
6
- # Used by the Github MCP Server
7
- GITHUB_PERSONAL_ACCESS_TOKEN=github_pat_REPLACE_ME
8
-
9
- # Used by the Google Drive MCP Server
10
- GOOGLE_CLIENT_ID=your_client_id_here.apps.googleusercontent.com
11
- GOOGLE_CLIENT_SECRET=your_client_secret_here
@@ -1,308 +0,0 @@
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