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.
@@ -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