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,269 @@
|
|
1
|
+
# Google Drive MCP Server
|
2
|
+
|
3
|
+
A Ruby-based Model Context Protocol (MCP) server that provides programmatic access to Google Drive. This server can operate in two modes:
|
4
|
+
|
5
|
+
1. **CLI script**: Direct command-line usage for searching, listing, and retrieving files
|
6
|
+
2. **MCP server**: Integration with AI assistants like Claude Code
|
7
|
+
|
8
|
+
## Features
|
9
|
+
|
10
|
+
- 🔍 **Search files** by content or name in Google Drive
|
11
|
+
- 📂 **List recent files** with metadata
|
12
|
+
- 📄 **Retrieve file content** with automatic format conversion
|
13
|
+
- 🔄 **Export Google Workspace documents**:
|
14
|
+
- Google Docs → Markdown
|
15
|
+
- Google Sheets → CSV
|
16
|
+
- Google Presentations → Plain text
|
17
|
+
- Google Drawings → PNG
|
18
|
+
- 🔐 **OAuth 2.0 authentication** with credential persistence
|
19
|
+
- 🛡️ **Error handling** with retry logic and comprehensive logging
|
20
|
+
|
21
|
+
## Prerequisites
|
22
|
+
|
23
|
+
### 1. Ruby Environment
|
24
|
+
|
25
|
+
```bash
|
26
|
+
# Install Ruby dependencies
|
27
|
+
bundle install
|
28
|
+
```
|
29
|
+
|
30
|
+
Required gems:
|
31
|
+
- `google-apis-drive_v3` - Google Drive API client
|
32
|
+
- `googleauth` - Google OAuth authentication
|
33
|
+
- `thor` - CLI framework
|
34
|
+
|
35
|
+
### 2. Google Cloud Platform Setup
|
36
|
+
|
37
|
+
#### Step 1: Create a Google Cloud Project
|
38
|
+
|
39
|
+
1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
|
40
|
+
2. Create a new project or select an existing one
|
41
|
+
3. Note your project ID
|
42
|
+
|
43
|
+
#### Step 2: Enable the Google Drive API
|
44
|
+
|
45
|
+
1. In the Cloud Console, go to **APIs & Services > Library**
|
46
|
+
2. Search for "Google Drive API"
|
47
|
+
3. Click on it and click **Enable**
|
48
|
+
|
49
|
+
#### Step 3: Create OAuth 2.0 Credentials
|
50
|
+
|
51
|
+
1. Go to **APIs & Services > Credentials**
|
52
|
+
2. Click **+ Create Credentials > OAuth client ID**
|
53
|
+
3. If prompted, configure the OAuth consent screen:
|
54
|
+
- Choose **External** user type
|
55
|
+
- Fill in required fields (app name, user support email)
|
56
|
+
- Add your email to test users
|
57
|
+
- Scopes: you can leave this empty for now
|
58
|
+
4. For Application type, choose **Desktop application**
|
59
|
+
5. Give it a name (e.g., "Google Drive MCP Server")
|
60
|
+
6. **Add Authorized redirect URI**: `http://localhost:8080`
|
61
|
+
7. Click **Create**
|
62
|
+
8. Download the JSON file containing your client ID and secret
|
63
|
+
|
64
|
+
#### Step 4: Configure OAuth Credentials
|
65
|
+
|
66
|
+
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.
|
67
|
+
|
68
|
+
**Note**: All Google services (Calendar, Drive, Meet) share the same authentication system, so you only need to authenticate once.
|
69
|
+
|
70
|
+
## Authentication
|
71
|
+
|
72
|
+
Before using the Google Drive MCP server, you need to authenticate with Google:
|
73
|
+
|
74
|
+
```bash
|
75
|
+
mcpz google auth
|
76
|
+
```
|
77
|
+
|
78
|
+
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.
|
79
|
+
|
80
|
+
## Usage
|
81
|
+
|
82
|
+
### CLI Mode
|
83
|
+
|
84
|
+
#### Test Connection
|
85
|
+
```bash
|
86
|
+
mcpz gdrive test
|
87
|
+
```
|
88
|
+
|
89
|
+
#### Search for Files
|
90
|
+
```bash
|
91
|
+
# Basic search
|
92
|
+
mcpz gdrive search "quarterly report"
|
93
|
+
|
94
|
+
# Limit results
|
95
|
+
mcpz gdrive search "meeting notes" --max-results 5
|
96
|
+
```
|
97
|
+
|
98
|
+
#### List Recent Files
|
99
|
+
```bash
|
100
|
+
# List 20 most recent files
|
101
|
+
mcpz gdrive list
|
102
|
+
|
103
|
+
# Limit results
|
104
|
+
mcpz gdrive list --max-results 10
|
105
|
+
```
|
106
|
+
|
107
|
+
#### Get File Content
|
108
|
+
```bash
|
109
|
+
# Display content in terminal
|
110
|
+
mcpz gdrive get "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms"
|
111
|
+
|
112
|
+
# Save to file
|
113
|
+
mcpz gdrive get "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms" --output document.md
|
114
|
+
```
|
115
|
+
|
116
|
+
### MCP Server Mode
|
117
|
+
|
118
|
+
#### Configuration for Claude Code
|
119
|
+
|
120
|
+
Add to your `.mcp.json` configuration:
|
121
|
+
|
122
|
+
```json
|
123
|
+
{
|
124
|
+
"mcpServers": {
|
125
|
+
"gdrive": {
|
126
|
+
"command": "mcpz",
|
127
|
+
"args": ["gdrive", "mcp"]
|
128
|
+
}
|
129
|
+
}
|
130
|
+
}
|
131
|
+
```
|
132
|
+
|
133
|
+
#### Run as Standalone MCP Server
|
134
|
+
|
135
|
+
```bash
|
136
|
+
mcpz gdrive mcp
|
137
|
+
```
|
138
|
+
|
139
|
+
The server provides these tools to Claude Code:
|
140
|
+
|
141
|
+
- **test_connection**: Test Google Drive API connectivity
|
142
|
+
- **search_files**: Search for files by content or name
|
143
|
+
- **get_file_content**: Retrieve content of a specific file
|
144
|
+
- **list_files**: List recent files in Google Drive
|
145
|
+
|
146
|
+
## File Format Support
|
147
|
+
|
148
|
+
### Google Workspace Documents
|
149
|
+
|
150
|
+
The server automatically exports Google Workspace documents to readable formats:
|
151
|
+
|
152
|
+
| Document Type | Export Format | File Extension |
|
153
|
+
|---------------|---------------|----------------|
|
154
|
+
| Google Docs | Markdown | `.md` |
|
155
|
+
| Google Sheets | CSV | `.csv` |
|
156
|
+
| Google Slides | Plain text | `.txt` |
|
157
|
+
| Google Drawings | PNG image | `.png` |
|
158
|
+
|
159
|
+
### Regular Files
|
160
|
+
|
161
|
+
All other file types (PDFs, images, text files, etc.) are downloaded in their original format.
|
162
|
+
|
163
|
+
## Security & Permissions
|
164
|
+
|
165
|
+
### Required OAuth Scopes
|
166
|
+
|
167
|
+
- `https://www.googleapis.com/auth/drive.readonly` - Read-only access to Google Drive
|
168
|
+
|
169
|
+
### Local File Storage
|
170
|
+
|
171
|
+
- **Credentials**: Stored in `~/.config/mcpeasy/google/token.json`
|
172
|
+
- **Logs**: Stored in `./logs/mcp_gdrive_*.log`
|
173
|
+
|
174
|
+
### Best Practices
|
175
|
+
|
176
|
+
1. **Never commit** credential files to version control
|
177
|
+
2. **Limit scope** to read-only access
|
178
|
+
3. **Regular rotation** of OAuth credentials (recommended annually)
|
179
|
+
4. **Monitor usage** through Google Cloud Console
|
180
|
+
|
181
|
+
## Troubleshooting
|
182
|
+
|
183
|
+
### Common Issues
|
184
|
+
|
185
|
+
#### "Authentication required" Error
|
186
|
+
- Run `mcpz google auth` to authenticate
|
187
|
+
- Check that `~/.config/mcpeasy/google/token.json` exists and is valid
|
188
|
+
- Verify your Google OAuth credentials are configured correctly
|
189
|
+
|
190
|
+
#### "Bad credentials" Error
|
191
|
+
- Regenerate OAuth credentials in Google Cloud Console
|
192
|
+
- Re-run authentication: `mcpz google auth`
|
193
|
+
|
194
|
+
#### "Quota exceeded" Error
|
195
|
+
- Check your Google Cloud Console for API quota limits
|
196
|
+
- Wait for quota reset (usually daily)
|
197
|
+
- Consider requesting higher quota limits
|
198
|
+
|
199
|
+
#### "Access denied" Error
|
200
|
+
- Ensure the Google Drive API is enabled in your project
|
201
|
+
- Check that your OAuth consent screen is properly configured
|
202
|
+
- Verify you're using the correct Google account
|
203
|
+
|
204
|
+
### Debug Logging
|
205
|
+
|
206
|
+
Check the log files for detailed error information:
|
207
|
+
|
208
|
+
```bash
|
209
|
+
# View recent errors
|
210
|
+
tail -f logs/mcp_gdrive_error.log
|
211
|
+
|
212
|
+
# View startup logs
|
213
|
+
tail -f logs/mcp_gdrive_startup.log
|
214
|
+
```
|
215
|
+
|
216
|
+
### Testing the Setup
|
217
|
+
|
218
|
+
1. **Test CLI authentication**:
|
219
|
+
```bash
|
220
|
+
mcpz gdrive test
|
221
|
+
```
|
222
|
+
|
223
|
+
2. **Test MCP server**:
|
224
|
+
```bash
|
225
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | mcpz gdrive mcp
|
226
|
+
```
|
227
|
+
|
228
|
+
## Development
|
229
|
+
|
230
|
+
### File Structure
|
231
|
+
|
232
|
+
```
|
233
|
+
utilities/gdrive/
|
234
|
+
├── cli.rb # Thor-based CLI interface
|
235
|
+
├── mcp.rb # MCP server implementation
|
236
|
+
├── gdrive_tool.rb # Google Drive API wrapper
|
237
|
+
└── README.md # This file
|
238
|
+
```
|
239
|
+
|
240
|
+
### Adding New Features
|
241
|
+
|
242
|
+
1. **New API methods**: Add to `GdriveTool` class
|
243
|
+
2. **New CLI commands**: Add to `GdriveCLI` class
|
244
|
+
3. **New MCP tools**: Add to `MCPServer` class
|
245
|
+
|
246
|
+
### Testing
|
247
|
+
|
248
|
+
```bash
|
249
|
+
# Run Ruby linting
|
250
|
+
bundle exec standardrb
|
251
|
+
|
252
|
+
# Test CLI commands
|
253
|
+
mcpz gdrive test
|
254
|
+
mcpz gdrive list --max-results 5
|
255
|
+
|
256
|
+
# Test MCP server manually
|
257
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | mcpz gdrive mcp
|
258
|
+
```
|
259
|
+
|
260
|
+
## Contributing
|
261
|
+
|
262
|
+
1. Follow existing code patterns and style
|
263
|
+
2. Add comprehensive error handling
|
264
|
+
3. Update this README for new features
|
265
|
+
4. Test both CLI and MCP modes
|
266
|
+
|
267
|
+
## License
|
268
|
+
|
269
|
+
This project follows the same license as the parent repository.
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "thor"
|
5
|
+
require_relative "gdrive_tool"
|
6
|
+
|
7
|
+
class GdriveCLI < Thor
|
8
|
+
desc "test", "Test the Google Drive API connection"
|
9
|
+
def test
|
10
|
+
response = tool.test_connection
|
11
|
+
|
12
|
+
if response[:ok]
|
13
|
+
puts "✅ Successfully connected to Google Drive"
|
14
|
+
puts " User: #{response[:user]} (#{response[:email]})"
|
15
|
+
if response[:storage_used] && response[:storage_limit]
|
16
|
+
puts " Storage: #{format_bytes(response[:storage_used])} / #{format_bytes(response[:storage_limit])}"
|
17
|
+
end
|
18
|
+
else
|
19
|
+
warn "❌ Connection test failed"
|
20
|
+
end
|
21
|
+
rescue RuntimeError => e
|
22
|
+
puts "❌ Failed to connect to Google Drive: #{e.message}"
|
23
|
+
exit 1
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "search QUERY", "Search for files in Google Drive"
|
27
|
+
method_option :max_results, type: :numeric, default: 10, aliases: "-n"
|
28
|
+
def search(query)
|
29
|
+
result = tool.search_files(query, max_results: options[:max_results])
|
30
|
+
files = result[:files]
|
31
|
+
|
32
|
+
if files.empty?
|
33
|
+
puts "🔍 No files found matching '#{query}'"
|
34
|
+
else
|
35
|
+
puts "🔍 Found #{result[:count]} file(s) matching '#{query}':"
|
36
|
+
files.each_with_index do |file, index|
|
37
|
+
puts " #{index + 1}. #{file[:name]}"
|
38
|
+
puts " ID: #{file[:id]}"
|
39
|
+
puts " Type: #{file[:mime_type]}"
|
40
|
+
puts " Size: #{format_bytes(file[:size])}"
|
41
|
+
puts " Modified: #{file[:modified_time]}"
|
42
|
+
puts " Link: #{file[:web_view_link]}"
|
43
|
+
puts
|
44
|
+
end
|
45
|
+
end
|
46
|
+
rescue RuntimeError => e
|
47
|
+
warn "❌ Failed to search files: #{e.message}"
|
48
|
+
exit 1
|
49
|
+
end
|
50
|
+
|
51
|
+
desc "list", "List recent files in Google Drive"
|
52
|
+
method_option :max_results, type: :numeric, default: 20, aliases: "-n"
|
53
|
+
def list
|
54
|
+
result = tool.list_files(max_results: options[:max_results])
|
55
|
+
files = result[:files]
|
56
|
+
|
57
|
+
if files.empty?
|
58
|
+
puts "📂 No files found in Google Drive"
|
59
|
+
else
|
60
|
+
puts "📂 Recent #{result[:count]} file(s):"
|
61
|
+
files.each_with_index do |file, index|
|
62
|
+
puts " #{index + 1}. #{file[:name]}"
|
63
|
+
puts " ID: #{file[:id]}"
|
64
|
+
puts " Type: #{file[:mime_type]}"
|
65
|
+
puts " Size: #{format_bytes(file[:size])}"
|
66
|
+
puts " Modified: #{file[:modified_time]}"
|
67
|
+
puts " Link: #{file[:web_view_link]}"
|
68
|
+
puts
|
69
|
+
end
|
70
|
+
end
|
71
|
+
rescue RuntimeError => e
|
72
|
+
warn "❌ Failed to list files: #{e.message}"
|
73
|
+
exit 1
|
74
|
+
end
|
75
|
+
|
76
|
+
desc "get FILE_ID", "Get content of a specific file"
|
77
|
+
method_option :output, type: :string, aliases: "-o", desc: "Output file path"
|
78
|
+
def get(file_id)
|
79
|
+
result = tool.get_file_content(file_id)
|
80
|
+
|
81
|
+
puts "📄 #{result[:name]}"
|
82
|
+
puts " Type: #{result[:mime_type]}"
|
83
|
+
puts " Size: #{format_bytes(result[:size])}"
|
84
|
+
puts
|
85
|
+
|
86
|
+
if options[:output]
|
87
|
+
File.write(options[:output], result[:content])
|
88
|
+
puts "✅ Content saved to #{options[:output]}"
|
89
|
+
else
|
90
|
+
puts "Content:"
|
91
|
+
puts result[:content]
|
92
|
+
end
|
93
|
+
rescue RuntimeError => e
|
94
|
+
warn "❌ Failed to get file content: #{e.message}"
|
95
|
+
exit 1
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def tool
|
101
|
+
@tool ||= GdriveTool.new
|
102
|
+
end
|
103
|
+
|
104
|
+
def format_bytes(bytes)
|
105
|
+
return "Unknown" unless bytes
|
106
|
+
|
107
|
+
units = %w[B KB MB GB TB]
|
108
|
+
size = bytes.to_f
|
109
|
+
unit_index = 0
|
110
|
+
|
111
|
+
while size >= 1024 && unit_index < units.length - 1
|
112
|
+
size /= 1024
|
113
|
+
unit_index += 1
|
114
|
+
end
|
115
|
+
|
116
|
+
"#{size.round(1)} #{units[unit_index]}"
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,291 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "google/apis/drive_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 GdriveTool
|
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
|
+
# MIME type mappings for Google Workspace documents
|
21
|
+
EXPORT_FORMATS = {
|
22
|
+
"application/vnd.google-apps.document" => {
|
23
|
+
format: "text/markdown",
|
24
|
+
extension: ".md"
|
25
|
+
},
|
26
|
+
"application/vnd.google-apps.spreadsheet" => {
|
27
|
+
format: "text/csv",
|
28
|
+
extension: ".csv"
|
29
|
+
},
|
30
|
+
"application/vnd.google-apps.presentation" => {
|
31
|
+
format: "text/plain",
|
32
|
+
extension: ".txt"
|
33
|
+
},
|
34
|
+
"application/vnd.google-apps.drawing" => {
|
35
|
+
format: "image/png",
|
36
|
+
extension: ".png"
|
37
|
+
}
|
38
|
+
}.freeze
|
39
|
+
|
40
|
+
def initialize(skip_auth: false)
|
41
|
+
ensure_env!
|
42
|
+
@service = Google::Apis::DriveV3::DriveService.new
|
43
|
+
@service.authorization = authorize unless skip_auth
|
44
|
+
end
|
45
|
+
|
46
|
+
def search_files(query, max_results: 10)
|
47
|
+
results = @service.list_files(
|
48
|
+
q: "fullText contains '#{query.gsub("'", "\\'")}' and trashed=false",
|
49
|
+
page_size: max_results,
|
50
|
+
fields: "files(id,name,mimeType,size,modifiedTime,webViewLink)"
|
51
|
+
)
|
52
|
+
|
53
|
+
files = results.files.map do |file|
|
54
|
+
{
|
55
|
+
id: file.id,
|
56
|
+
name: file.name,
|
57
|
+
mime_type: file.mime_type,
|
58
|
+
size: file.size&.to_i,
|
59
|
+
modified_time: file.modified_time,
|
60
|
+
web_view_link: file.web_view_link
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
{files: files, count: files.length}
|
65
|
+
rescue Google::Apis::Error => e
|
66
|
+
raise "Google Drive API Error: #{e.message}"
|
67
|
+
rescue => e
|
68
|
+
log_error("search_files", e)
|
69
|
+
raise e
|
70
|
+
end
|
71
|
+
|
72
|
+
def get_file_content(file_id)
|
73
|
+
# First get file metadata
|
74
|
+
file = @service.get_file(file_id, fields: "id,name,mimeType,size")
|
75
|
+
|
76
|
+
content = if EXPORT_FORMATS.key?(file.mime_type)
|
77
|
+
# Export Google Workspace document
|
78
|
+
export_format = EXPORT_FORMATS[file.mime_type][:format]
|
79
|
+
@service.export_file(file_id, export_format)
|
80
|
+
else
|
81
|
+
# Download regular file
|
82
|
+
@service.get_file(file_id, download_dest: StringIO.new)
|
83
|
+
end
|
84
|
+
|
85
|
+
{
|
86
|
+
id: file.id,
|
87
|
+
name: file.name,
|
88
|
+
mime_type: file.mime_type,
|
89
|
+
size: file.size&.to_i,
|
90
|
+
content: content.is_a?(StringIO) ? content.string : content
|
91
|
+
}
|
92
|
+
rescue Google::Apis::Error => e
|
93
|
+
raise "Google Drive API Error: #{e.message}"
|
94
|
+
rescue => e
|
95
|
+
log_error("get_file_content", e)
|
96
|
+
raise e
|
97
|
+
end
|
98
|
+
|
99
|
+
def list_files(max_results: 20)
|
100
|
+
results = @service.list_files(
|
101
|
+
q: "trashed=false",
|
102
|
+
page_size: max_results,
|
103
|
+
order_by: "modifiedTime desc",
|
104
|
+
fields: "files(id,name,mimeType,size,modifiedTime,webViewLink)"
|
105
|
+
)
|
106
|
+
|
107
|
+
files = results.files.map do |file|
|
108
|
+
{
|
109
|
+
id: file.id,
|
110
|
+
name: file.name,
|
111
|
+
mime_type: file.mime_type,
|
112
|
+
size: file.size&.to_i,
|
113
|
+
modified_time: file.modified_time,
|
114
|
+
web_view_link: file.web_view_link
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
{files: files, count: files.length}
|
119
|
+
rescue Google::Apis::Error => e
|
120
|
+
raise "Google Drive API Error: #{e.message}"
|
121
|
+
rescue => e
|
122
|
+
log_error("list_files", e)
|
123
|
+
raise e
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_connection
|
127
|
+
about = @service.get_about(fields: "user,storageQuota")
|
128
|
+
{
|
129
|
+
ok: true,
|
130
|
+
user: about.user.display_name,
|
131
|
+
email: about.user.email_address,
|
132
|
+
storage_used: about.storage_quota&.usage,
|
133
|
+
storage_limit: about.storage_quota&.limit
|
134
|
+
}
|
135
|
+
rescue Google::Apis::Error => e
|
136
|
+
raise "Google Drive API Error: #{e.message}"
|
137
|
+
rescue => e
|
138
|
+
log_error("test_connection", e)
|
139
|
+
raise e
|
140
|
+
end
|
141
|
+
|
142
|
+
def authenticate
|
143
|
+
perform_auth_flow
|
144
|
+
{success: true}
|
145
|
+
rescue => e
|
146
|
+
{success: false, error: e.message}
|
147
|
+
end
|
148
|
+
|
149
|
+
def perform_auth_flow
|
150
|
+
client_id = Mcpeasy::Config.google_client_id
|
151
|
+
client_secret = Mcpeasy::Config.google_client_secret
|
152
|
+
|
153
|
+
unless client_id && client_secret
|
154
|
+
raise "Google credentials not found. Please save your credentials.json file using: mcpz config set_google_credentials <path_to_credentials.json>"
|
155
|
+
end
|
156
|
+
|
157
|
+
# Create credentials using OAuth2 flow with localhost redirect
|
158
|
+
redirect_uri = "http://localhost:8080"
|
159
|
+
client = Signet::OAuth2::Client.new(
|
160
|
+
client_id: client_id,
|
161
|
+
client_secret: client_secret,
|
162
|
+
scope: SCOPE,
|
163
|
+
redirect_uri: redirect_uri,
|
164
|
+
authorization_uri: "https://accounts.google.com/o/oauth2/auth",
|
165
|
+
token_credential_uri: "https://oauth2.googleapis.com/token"
|
166
|
+
)
|
167
|
+
|
168
|
+
# Generate authorization URL
|
169
|
+
url = client.authorization_uri.to_s
|
170
|
+
|
171
|
+
puts "DEBUG: Client ID: #{client_id[0..20]}..."
|
172
|
+
puts "DEBUG: Scope: #{SCOPE}"
|
173
|
+
puts "DEBUG: Redirect URI: #{redirect_uri}"
|
174
|
+
puts
|
175
|
+
|
176
|
+
# Start callback server to capture OAuth code
|
177
|
+
puts "Starting temporary web server to capture OAuth callback..."
|
178
|
+
puts "Opening authorization URL in your default browser..."
|
179
|
+
puts url
|
180
|
+
puts
|
181
|
+
|
182
|
+
# Automatically open URL in default browser on macOS/Unix
|
183
|
+
if system("which open > /dev/null 2>&1")
|
184
|
+
system("open", url)
|
185
|
+
else
|
186
|
+
puts "Could not automatically open browser. Please copy the URL above manually."
|
187
|
+
end
|
188
|
+
puts
|
189
|
+
puts "Waiting for OAuth callback... (will timeout in 60 seconds)"
|
190
|
+
|
191
|
+
# Wait for the authorization code with timeout
|
192
|
+
code = GoogleAuthServer.capture_auth_code
|
193
|
+
|
194
|
+
unless code
|
195
|
+
raise "Failed to receive authorization code. Please try again."
|
196
|
+
end
|
197
|
+
|
198
|
+
puts "✅ Authorization code received!"
|
199
|
+
client.code = code
|
200
|
+
client.fetch_access_token!
|
201
|
+
|
202
|
+
# Save credentials to config
|
203
|
+
credentials_data = {
|
204
|
+
client_id: client.client_id,
|
205
|
+
client_secret: client.client_secret,
|
206
|
+
scope: client.scope,
|
207
|
+
refresh_token: client.refresh_token,
|
208
|
+
access_token: client.access_token,
|
209
|
+
expires_at: client.expires_at
|
210
|
+
}
|
211
|
+
|
212
|
+
Mcpeasy::Config.save_google_token(credentials_data)
|
213
|
+
puts "✅ Authentication successful! Token saved to config"
|
214
|
+
|
215
|
+
client
|
216
|
+
rescue => e
|
217
|
+
log_error("perform_auth_flow", e)
|
218
|
+
raise "Authentication flow failed: #{e.message}"
|
219
|
+
end
|
220
|
+
|
221
|
+
private
|
222
|
+
|
223
|
+
def authorize
|
224
|
+
credentials_data = Mcpeasy::Config.google_token
|
225
|
+
unless credentials_data
|
226
|
+
raise <<~ERROR
|
227
|
+
Google Drive authentication required!
|
228
|
+
Run the auth command first:
|
229
|
+
mcpz gdrive auth
|
230
|
+
ERROR
|
231
|
+
end
|
232
|
+
|
233
|
+
client = Signet::OAuth2::Client.new(
|
234
|
+
client_id: credentials_data.client_id,
|
235
|
+
client_secret: credentials_data.client_secret,
|
236
|
+
scope: credentials_data.scope.respond_to?(:to_a) ? credentials_data.scope.to_a.join(" ") : credentials_data.scope.to_s,
|
237
|
+
refresh_token: credentials_data.refresh_token,
|
238
|
+
access_token: credentials_data.access_token,
|
239
|
+
token_credential_uri: "https://oauth2.googleapis.com/token"
|
240
|
+
)
|
241
|
+
|
242
|
+
# Check if token needs refresh
|
243
|
+
if credentials_data.expires_at
|
244
|
+
expires_at = if credentials_data.expires_at.is_a?(String)
|
245
|
+
Time.parse(credentials_data.expires_at)
|
246
|
+
else
|
247
|
+
Time.at(credentials_data.expires_at)
|
248
|
+
end
|
249
|
+
|
250
|
+
if Time.now >= expires_at
|
251
|
+
client.refresh!
|
252
|
+
# Update saved credentials with new access token
|
253
|
+
updated_data = {
|
254
|
+
client_id: credentials_data.client_id,
|
255
|
+
client_secret: credentials_data.client_secret,
|
256
|
+
scope: credentials_data.scope.respond_to?(:to_a) ? credentials_data.scope.to_a.join(" ") : credentials_data.scope.to_s,
|
257
|
+
refresh_token: credentials_data.refresh_token,
|
258
|
+
access_token: client.access_token,
|
259
|
+
expires_at: client.expires_at
|
260
|
+
}
|
261
|
+
Mcpeasy::Config.save_google_token(updated_data)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
client
|
266
|
+
rescue JSON::ParserError
|
267
|
+
raise "Invalid token data. Please re-run: mcpz gdrive auth"
|
268
|
+
rescue => e
|
269
|
+
log_error("authorize", e)
|
270
|
+
raise "Authentication failed: #{e.message}"
|
271
|
+
end
|
272
|
+
|
273
|
+
def ensure_env!
|
274
|
+
unless Mcpeasy::Config.google_client_id && Mcpeasy::Config.google_client_secret
|
275
|
+
raise <<~ERROR
|
276
|
+
Google API credentials not configured!
|
277
|
+
Please save your Google credentials.json file using:
|
278
|
+
mcpz config set_google_credentials <path_to_credentials.json>
|
279
|
+
ERROR
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def log_error(method, error)
|
284
|
+
Mcpeasy::Config.ensure_config_dirs
|
285
|
+
File.write(
|
286
|
+
Mcpeasy::Config.log_file_path("gdrive", "error"),
|
287
|
+
"#{Time.now}: #{method} error: #{error.class}: #{error.message}\n#{error.backtrace.join("\n")}\n",
|
288
|
+
mode: "a"
|
289
|
+
)
|
290
|
+
end
|
291
|
+
end
|