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,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require_relative "config"
|
5
|
+
|
6
|
+
module Mcpeasy
|
7
|
+
class Setup
|
8
|
+
def self.create_config_directories
|
9
|
+
puts "Setting up mcpeasy configuration directories..."
|
10
|
+
|
11
|
+
# Use Config class to create all directories including logs
|
12
|
+
Config.ensure_config_dirs
|
13
|
+
|
14
|
+
puts "Created #{Config::CONFIG_DIR}"
|
15
|
+
puts "Created #{Config::GOOGLE_DIR}"
|
16
|
+
puts "Created #{Config::SLACK_DIR}"
|
17
|
+
puts "Created #{Config::LOGS_DIR}"
|
18
|
+
|
19
|
+
puts "mcpeasy setup complete!"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/mcpeasy.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webrick"
|
4
|
+
require "timeout"
|
5
|
+
|
6
|
+
class GoogleAuthServer
|
7
|
+
def self.capture_auth_code(port: 8080, timeout: 60)
|
8
|
+
new(port: port, timeout: timeout).capture_auth_code
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(port: 8080, timeout: 60)
|
12
|
+
@port = port
|
13
|
+
@timeout = timeout
|
14
|
+
@auth_code = nil
|
15
|
+
@auth_received = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def capture_auth_code
|
19
|
+
server, server_thread = start_callback_server
|
20
|
+
wait_for_auth_code(server, server_thread)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def start_callback_server
|
26
|
+
server = WEBrick::HTTPServer.new(
|
27
|
+
Port: @port,
|
28
|
+
Logger: WEBrick::Log.new(File::NULL),
|
29
|
+
AccessLog: [],
|
30
|
+
BindAddress: "127.0.0.1"
|
31
|
+
)
|
32
|
+
|
33
|
+
server.mount_proc("/") do |req, res|
|
34
|
+
if req.query["code"]
|
35
|
+
@auth_code = req.query["code"]
|
36
|
+
@auth_received = true
|
37
|
+
res.content_type = "text/html"
|
38
|
+
res.body = success_html
|
39
|
+
schedule_shutdown(server)
|
40
|
+
elsif req.query["error"]
|
41
|
+
@auth_received = true
|
42
|
+
res.content_type = "text/html"
|
43
|
+
res.body = error_html(req.query["error"])
|
44
|
+
schedule_shutdown(server)
|
45
|
+
else
|
46
|
+
res.content_type = "text/html"
|
47
|
+
res.body = waiting_html
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
server_thread = Thread.new do
|
52
|
+
server.start
|
53
|
+
rescue => e
|
54
|
+
puts "Server error: #{e.message}" unless e.message.include?("shutdown")
|
55
|
+
end
|
56
|
+
|
57
|
+
sleep 0.1
|
58
|
+
[server, server_thread]
|
59
|
+
rescue => e
|
60
|
+
raise "Failed to start callback server: #{e.message}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def wait_for_auth_code(server, server_thread)
|
64
|
+
begin
|
65
|
+
Timeout.timeout(@timeout) do
|
66
|
+
until @auth_received
|
67
|
+
sleep 0.1
|
68
|
+
break unless server_thread.alive?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
rescue Timeout::Error
|
72
|
+
puts "\n⏰ Timeout waiting for authorization. Please try again."
|
73
|
+
return nil
|
74
|
+
ensure
|
75
|
+
begin
|
76
|
+
server&.shutdown
|
77
|
+
server_thread&.join(2)
|
78
|
+
rescue
|
79
|
+
# Ignore shutdown errors
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
@auth_code
|
84
|
+
end
|
85
|
+
|
86
|
+
def schedule_shutdown(server)
|
87
|
+
Thread.new do
|
88
|
+
sleep 0.5
|
89
|
+
server.shutdown
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def success_html
|
94
|
+
<<~HTML
|
95
|
+
<!DOCTYPE html>
|
96
|
+
<html>
|
97
|
+
<head>
|
98
|
+
<title>Authorization Successful</title>
|
99
|
+
<style>
|
100
|
+
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
101
|
+
.success { color: #28a745; }
|
102
|
+
</style>
|
103
|
+
</head>
|
104
|
+
<body>
|
105
|
+
<h1 class="success">✓ Authorization Successful!</h1>
|
106
|
+
<p>You can now close this window and return to your terminal.</p>
|
107
|
+
</body>
|
108
|
+
</html>
|
109
|
+
HTML
|
110
|
+
end
|
111
|
+
|
112
|
+
def error_html(error)
|
113
|
+
<<~HTML
|
114
|
+
<!DOCTYPE html>
|
115
|
+
<html>
|
116
|
+
<head>
|
117
|
+
<title>Authorization Failed</title>
|
118
|
+
<style>
|
119
|
+
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
120
|
+
.error { color: #dc3545; }
|
121
|
+
</style>
|
122
|
+
</head>
|
123
|
+
<body>
|
124
|
+
<h1 class="error">✗ Authorization Failed</h1>
|
125
|
+
<p>Error: #{error}</p>
|
126
|
+
<p>Please try again from your terminal.</p>
|
127
|
+
</body>
|
128
|
+
</html>
|
129
|
+
HTML
|
130
|
+
end
|
131
|
+
|
132
|
+
def waiting_html
|
133
|
+
<<~HTML
|
134
|
+
<!DOCTYPE html>
|
135
|
+
<html>
|
136
|
+
<head>
|
137
|
+
<title>Waiting for Authorization</title>
|
138
|
+
<style>
|
139
|
+
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
140
|
+
</style>
|
141
|
+
</head>
|
142
|
+
<body>
|
143
|
+
<h1>Waiting for Authorization...</h1>
|
144
|
+
<p>Please complete the authorization process.</p>
|
145
|
+
</body>
|
146
|
+
</html>
|
147
|
+
HTML
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
# Google Calendar MCP Server
|
2
|
+
|
3
|
+
A Ruby-based Model Context Protocol (MCP) server that provides programmatic access to Google Calendar. This server can operate in two modes:
|
4
|
+
|
5
|
+
1. **CLI script**: Direct command-line usage for listing events and calendar information
|
6
|
+
2. **MCP server**: Integration with AI assistants like Claude Code
|
7
|
+
|
8
|
+
## Features
|
9
|
+
|
10
|
+
- 📅 **List events** from your Google Calendar with date range filtering
|
11
|
+
- 📋 **List calendars** and view calendar metadata
|
12
|
+
- 🔍 **Search events** by text content
|
13
|
+
- 🔐 **OAuth 2.0 authentication** with credential persistence (shares credentials with gdrive utility)
|
14
|
+
- 🛡️ **Error handling** with retry logic and comprehensive logging
|
15
|
+
|
16
|
+
## Prerequisites
|
17
|
+
|
18
|
+
### 1. Ruby Environment
|
19
|
+
|
20
|
+
```bash
|
21
|
+
# Install Ruby dependencies
|
22
|
+
bundle install
|
23
|
+
```
|
24
|
+
|
25
|
+
Required gems:
|
26
|
+
- `google-apis-calendar_v3` - Google Calendar API client
|
27
|
+
- `googleauth` - Google OAuth authentication
|
28
|
+
- `thor` - CLI framework
|
29
|
+
|
30
|
+
### 2. Google Cloud Platform Setup
|
31
|
+
|
32
|
+
#### Step 1: Create a Google Cloud Project
|
33
|
+
|
34
|
+
1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
|
35
|
+
2. Create a new project or select an existing one
|
36
|
+
3. Note your project ID
|
37
|
+
|
38
|
+
#### Step 2: Enable the Google Calendar API
|
39
|
+
|
40
|
+
1. In the Cloud Console, go to **APIs & Services > Library**
|
41
|
+
2. Search for "Google Calendar API"
|
42
|
+
3. Click on it and click **Enable**
|
43
|
+
|
44
|
+
#### Step 3: Create OAuth 2.0 Credentials
|
45
|
+
|
46
|
+
1. Go to **APIs & Services > Credentials**
|
47
|
+
2. Click **+ Create Credentials > OAuth client ID**
|
48
|
+
3. If prompted, configure the OAuth consent screen:
|
49
|
+
- Choose **External** user type
|
50
|
+
- Fill in required fields (app name, user support email)
|
51
|
+
- Add your email to test users
|
52
|
+
- **Scopes**: Add `https://www.googleapis.com/auth/calendar.readonly`
|
53
|
+
4. For Application type, choose **Desktop application**
|
54
|
+
5. Give it a name (e.g., "Google Calendar MCP Server")
|
55
|
+
6. **Add Authorized redirect URI**: `http://localhost:8080`
|
56
|
+
7. Click **Create**
|
57
|
+
8. Download the JSON file containing your client ID and secret
|
58
|
+
|
59
|
+
#### Step 4: Configure OAuth Credentials
|
60
|
+
|
61
|
+
The Google OAuth credentials will be configured during the authentication process. You'll need the Client ID and Client Secret from the JSON file you downloaded in Step 3.
|
62
|
+
|
63
|
+
**Note**: All Google services (Calendar, Drive, Meet) share the same authentication system, so you only need to authenticate once.
|
64
|
+
|
65
|
+
## Authentication
|
66
|
+
|
67
|
+
Before using the Google Calendar MCP server, you need to authenticate with Google:
|
68
|
+
|
69
|
+
```bash
|
70
|
+
mcpz google auth
|
71
|
+
```
|
72
|
+
|
73
|
+
This will open a browser for Google OAuth authorization and save credentials to `~/.config/mcpeasy/google/token.json`. The credentials are shared with all Google services and will be automatically refreshed when needed.
|
74
|
+
|
75
|
+
## Usage
|
76
|
+
|
77
|
+
### CLI Mode
|
78
|
+
|
79
|
+
#### Test Connection
|
80
|
+
```bash
|
81
|
+
mcpz gcal test
|
82
|
+
```
|
83
|
+
|
84
|
+
#### List Events
|
85
|
+
```bash
|
86
|
+
# List today's events
|
87
|
+
mcpz gcal events
|
88
|
+
|
89
|
+
# List events for a specific date range
|
90
|
+
mcpz gcal events --start "2024-01-01" --end "2024-01-31"
|
91
|
+
|
92
|
+
# Limit results
|
93
|
+
mcpz gcal events --max-results 10
|
94
|
+
```
|
95
|
+
|
96
|
+
#### List Calendars
|
97
|
+
```bash
|
98
|
+
mcpz gcal calendars
|
99
|
+
```
|
100
|
+
|
101
|
+
### MCP Server Mode
|
102
|
+
|
103
|
+
#### Configuration for Claude Code
|
104
|
+
|
105
|
+
Add to your `.mcp.json` configuration:
|
106
|
+
|
107
|
+
```json
|
108
|
+
{
|
109
|
+
"mcpServers": {
|
110
|
+
"gcal": {
|
111
|
+
"command": "mcpz",
|
112
|
+
"args": ["gcal", "mcp"]
|
113
|
+
}
|
114
|
+
}
|
115
|
+
}
|
116
|
+
```
|
117
|
+
|
118
|
+
#### Run as Standalone MCP Server
|
119
|
+
|
120
|
+
```bash
|
121
|
+
mcpz gcal mcp
|
122
|
+
```
|
123
|
+
|
124
|
+
The server provides these tools to Claude Code:
|
125
|
+
|
126
|
+
- **test_connection**: Test Google Calendar API connectivity
|
127
|
+
- **list_events**: List calendar events with optional date filtering
|
128
|
+
- **list_calendars**: List available calendars
|
129
|
+
- **search_events**: Search for events by text content
|
130
|
+
|
131
|
+
## Security & Permissions
|
132
|
+
|
133
|
+
### Required OAuth Scopes
|
134
|
+
|
135
|
+
- `https://www.googleapis.com/auth/calendar.readonly` - Read-only access to Google Calendar
|
136
|
+
|
137
|
+
### Local File Storage
|
138
|
+
|
139
|
+
- **Credentials**: Stored in `~/.config/mcpeasy/google/token.json`
|
140
|
+
- **Logs**: Stored in `./logs/mcp_gcal_*.log`
|
141
|
+
|
142
|
+
### Best Practices
|
143
|
+
|
144
|
+
1. **Never commit** credential files to version control
|
145
|
+
2. **Limit scope** to read-only access
|
146
|
+
3. **Regular rotation** of OAuth credentials (recommended annually)
|
147
|
+
4. **Monitor usage** through Google Cloud Console
|
148
|
+
|
149
|
+
## Troubleshooting
|
150
|
+
|
151
|
+
### Common Issues
|
152
|
+
|
153
|
+
#### "Authentication required" Error
|
154
|
+
- Run `mcpz google auth` to authenticate
|
155
|
+
- Check that `~/.config/mcpeasy/google/token.json` exists and is valid
|
156
|
+
- Verify your Google OAuth credentials are configured correctly
|
157
|
+
|
158
|
+
#### "Bad credentials" Error
|
159
|
+
- Regenerate OAuth credentials in Google Cloud Console
|
160
|
+
- Re-run authentication: `mcpz google auth`
|
161
|
+
|
162
|
+
#### "Calendar API has not been used" Error
|
163
|
+
- Ensure the Google Calendar API is enabled in your project
|
164
|
+
- Go to Google Cloud Console > APIs & Services > Library
|
165
|
+
- Search for "Google Calendar API" and enable it
|
166
|
+
|
167
|
+
#### "Access denied" Error
|
168
|
+
- Ensure the Google Calendar API is enabled in your project
|
169
|
+
- Check that your OAuth consent screen includes the calendar.readonly scope
|
170
|
+
- Verify you're using the correct Google account
|
171
|
+
|
172
|
+
### Debug Logging
|
173
|
+
|
174
|
+
Check the log files for detailed error information:
|
175
|
+
|
176
|
+
```bash
|
177
|
+
# View recent errors
|
178
|
+
tail -f logs/mcp_gcal_error.log
|
179
|
+
|
180
|
+
# View startup logs
|
181
|
+
tail -f logs/mcp_gcal_startup.log
|
182
|
+
```
|
183
|
+
|
184
|
+
### Testing the Setup
|
185
|
+
|
186
|
+
1. **Test CLI authentication**:
|
187
|
+
```bash
|
188
|
+
mcpz gcal test
|
189
|
+
```
|
190
|
+
|
191
|
+
2. **Test MCP server**:
|
192
|
+
```bash
|
193
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | mcpz gcal mcp
|
194
|
+
```
|
195
|
+
|
196
|
+
## Development
|
197
|
+
|
198
|
+
### File Structure
|
199
|
+
|
200
|
+
```
|
201
|
+
utilities/gcal/
|
202
|
+
├── cli.rb # Thor-based CLI interface
|
203
|
+
├── mcp.rb # MCP server implementation
|
204
|
+
├── gcal_tool.rb # Google Calendar API wrapper
|
205
|
+
└── README.md # This file
|
206
|
+
```
|
207
|
+
|
208
|
+
### Adding New Features
|
209
|
+
|
210
|
+
1. **New API methods**: Add to `GcalTool` class
|
211
|
+
2. **New CLI commands**: Add to `GcalCLI` class
|
212
|
+
3. **New MCP tools**: Add to `MCPServer` class
|
213
|
+
|
214
|
+
### Testing
|
215
|
+
|
216
|
+
```bash
|
217
|
+
# Run Ruby linting
|
218
|
+
bundle exec standardrb
|
219
|
+
|
220
|
+
# Test CLI commands
|
221
|
+
mcpz gcal test
|
222
|
+
mcpz gcal events --max-results 5
|
223
|
+
|
224
|
+
# Test MCP server manually
|
225
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | mcpz gcal mcp
|
226
|
+
```
|
227
|
+
|
228
|
+
## Contributing
|
229
|
+
|
230
|
+
1. Follow existing code patterns and style
|
231
|
+
2. Add comprehensive error handling
|
232
|
+
3. Update this README for new features
|
233
|
+
4. Test both CLI and MCP modes
|
234
|
+
|
235
|
+
## License
|
236
|
+
|
237
|
+
This project follows the same license as the parent repository.
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "thor"
|
5
|
+
require_relative "gcal_tool"
|
6
|
+
|
7
|
+
class GcalCLI < Thor
|
8
|
+
desc "test", "Test the Google Calendar API connection"
|
9
|
+
def test
|
10
|
+
response = tool.test_connection
|
11
|
+
|
12
|
+
if response[:ok]
|
13
|
+
puts "✅ Successfully connected to Google Calendar"
|
14
|
+
puts " User: #{response[:user]} (#{response[:email]})"
|
15
|
+
else
|
16
|
+
warn "❌ Connection test failed"
|
17
|
+
end
|
18
|
+
rescue RuntimeError => e
|
19
|
+
puts "❌ Failed to connect to Google Calendar: #{e.message}\n\n#{e.backtrace.join("\n")}"
|
20
|
+
exit 1
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "events", "List calendar events"
|
24
|
+
method_option :start_date, type: :string, aliases: "-s", desc: "Start date (YYYY-MM-DD)"
|
25
|
+
method_option :end_date, type: :string, aliases: "-e", desc: "End date (YYYY-MM-DD)"
|
26
|
+
method_option :max_results, type: :numeric, default: 20, aliases: "-n", desc: "Max number of events"
|
27
|
+
method_option :calendar_id, type: :string, aliases: "-c", desc: "Calendar ID (default: primary)"
|
28
|
+
def events
|
29
|
+
result = tool.list_events(
|
30
|
+
start_date: options[:start_date],
|
31
|
+
end_date: options[:end_date],
|
32
|
+
max_results: options[:max_results],
|
33
|
+
calendar_id: options[:calendar_id] || "primary"
|
34
|
+
)
|
35
|
+
events = result[:events]
|
36
|
+
|
37
|
+
if events.empty?
|
38
|
+
puts "📅 No events found for the specified date range"
|
39
|
+
else
|
40
|
+
puts "📅 Found #{result[:count]} event(s):"
|
41
|
+
events.each_with_index do |event, index|
|
42
|
+
puts " #{index + 1}. #{event[:summary] || "No title"}"
|
43
|
+
puts " Start: #{format_datetime(event[:start])}"
|
44
|
+
puts " End: #{format_datetime(event[:end])}"
|
45
|
+
puts " Description: #{event[:description]}" if event[:description]
|
46
|
+
puts " Location: #{event[:location]}" if event[:location]
|
47
|
+
puts " Attendees: #{event[:attendees].join(", ")}" if event[:attendees]&.any?
|
48
|
+
puts " Link: #{event[:html_link]}"
|
49
|
+
puts
|
50
|
+
end
|
51
|
+
end
|
52
|
+
rescue RuntimeError => e
|
53
|
+
warn "❌ Failed to list events: #{e.message}"
|
54
|
+
exit 1
|
55
|
+
end
|
56
|
+
|
57
|
+
desc "calendars", "List available calendars"
|
58
|
+
def calendars
|
59
|
+
result = tool.list_calendars
|
60
|
+
calendars = result[:calendars]
|
61
|
+
|
62
|
+
if calendars.empty?
|
63
|
+
puts "📋 No calendars found"
|
64
|
+
else
|
65
|
+
puts "📋 Found #{result[:count]} calendar(s):"
|
66
|
+
calendars.each_with_index do |calendar, index|
|
67
|
+
puts " #{index + 1}. #{calendar[:summary]}"
|
68
|
+
puts " ID: #{calendar[:id]}"
|
69
|
+
puts " Description: #{calendar[:description]}" if calendar[:description]
|
70
|
+
puts " Time Zone: #{calendar[:time_zone]}"
|
71
|
+
puts " Access Role: #{calendar[:access_role]}"
|
72
|
+
puts " Primary: Yes" if calendar[:primary]
|
73
|
+
puts
|
74
|
+
end
|
75
|
+
end
|
76
|
+
rescue RuntimeError => e
|
77
|
+
warn "❌ Failed to list calendars: #{e.message}"
|
78
|
+
exit 1
|
79
|
+
end
|
80
|
+
|
81
|
+
desc "search QUERY", "Search for events by text content"
|
82
|
+
method_option :start_date, type: :string, aliases: "-s", desc: "Start date (YYYY-MM-DD)"
|
83
|
+
method_option :end_date, type: :string, aliases: "-e", desc: "End date (YYYY-MM-DD)"
|
84
|
+
method_option :max_results, type: :numeric, default: 10, aliases: "-n", desc: "Max number of events"
|
85
|
+
def search(query)
|
86
|
+
result = tool.search_events(
|
87
|
+
query,
|
88
|
+
start_date: options[:start_date],
|
89
|
+
end_date: options[:end_date],
|
90
|
+
max_results: options[:max_results]
|
91
|
+
)
|
92
|
+
events = result[:events]
|
93
|
+
|
94
|
+
if events.empty?
|
95
|
+
puts "🔍 No events found matching '#{query}'"
|
96
|
+
else
|
97
|
+
puts "🔍 Found #{result[:count]} event(s) matching '#{query}':"
|
98
|
+
events.each_with_index do |event, index|
|
99
|
+
puts " #{index + 1}. #{event[:summary] || "No title"}"
|
100
|
+
puts " Start: #{format_datetime(event[:start])}"
|
101
|
+
puts " End: #{format_datetime(event[:end])}"
|
102
|
+
puts " Description: #{event[:description]}" if event[:description]
|
103
|
+
puts " Location: #{event[:location]}" if event[:location]
|
104
|
+
puts " Calendar: #{event[:calendar_id]}"
|
105
|
+
puts " Link: #{event[:html_link]}"
|
106
|
+
puts
|
107
|
+
end
|
108
|
+
end
|
109
|
+
rescue RuntimeError => e
|
110
|
+
warn "❌ Failed to search events: #{e.message}"
|
111
|
+
exit 1
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def tool
|
117
|
+
@tool ||= GcalTool.new
|
118
|
+
end
|
119
|
+
|
120
|
+
def format_datetime(datetime_info)
|
121
|
+
return "Unknown" unless datetime_info
|
122
|
+
|
123
|
+
if datetime_info[:date]
|
124
|
+
# All-day event
|
125
|
+
datetime_info[:date]
|
126
|
+
elsif datetime_info[:date_time]
|
127
|
+
# Specific time event
|
128
|
+
time = Time.parse(datetime_info[:date_time])
|
129
|
+
time.strftime("%Y-%m-%d %H:%M")
|
130
|
+
else
|
131
|
+
"Unknown"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|