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.
- checksums.yaml +4 -4
- data/.claudeignore +0 -3
- data/.mcp.json +19 -1
- data/CHANGELOG.md +59 -0
- data/CLAUDE.md +19 -5
- data/README.md +19 -3
- data/lib/mcpeasy/cli.rb +62 -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/gmail/README.md +278 -0
- data/lib/utilities/gmail/cli.rb +264 -0
- data/lib/utilities/gmail/mcp.rb +846 -0
- data/lib/utilities/gmail/service.rb +547 -0
- data/lib/utilities/gmeet/cli.rb +131 -129
- data/lib/utilities/gmeet/mcp.rb +374 -372
- data/lib/utilities/gmeet/service.rb +411 -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
- data/mcpeasy.gemspec +6 -1
- metadata +87 -10
- 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
@@ -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.
|
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,
|
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/
|
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.
|
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
|