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,347 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "json"
6
+ require_relative "gdrive_tool"
7
+
8
+ class MCPServer
9
+ def initialize
10
+ @tools = {
11
+ "test_connection" => {
12
+ name: "test_connection",
13
+ description: "Test the Google Drive API connection",
14
+ inputSchema: {
15
+ type: "object",
16
+ properties: {},
17
+ required: []
18
+ }
19
+ },
20
+ "search_files" => {
21
+ name: "search_files",
22
+ description: "Search for files in Google Drive by content or name",
23
+ inputSchema: {
24
+ type: "object",
25
+ properties: {
26
+ query: {
27
+ type: "string",
28
+ description: "Search query to find files"
29
+ },
30
+ max_results: {
31
+ type: "number",
32
+ description: "Maximum number of results to return (default: 10)"
33
+ }
34
+ },
35
+ required: ["query"]
36
+ }
37
+ },
38
+ "get_file_content" => {
39
+ name: "get_file_content",
40
+ description: "Get the content of a specific Google Drive file",
41
+ inputSchema: {
42
+ type: "object",
43
+ properties: {
44
+ file_id: {
45
+ type: "string",
46
+ description: "The Google Drive file ID"
47
+ }
48
+ },
49
+ required: ["file_id"]
50
+ }
51
+ },
52
+ "list_files" => {
53
+ name: "list_files",
54
+ description: "List recent files in Google Drive",
55
+ inputSchema: {
56
+ type: "object",
57
+ properties: {
58
+ max_results: {
59
+ type: "number",
60
+ description: "Maximum number of files to return (default: 20)"
61
+ }
62
+ },
63
+ required: []
64
+ }
65
+ }
66
+ }
67
+ end
68
+
69
+ def run
70
+ # Disable stdout buffering for immediate response
71
+ $stdout.sync = true
72
+
73
+ # Log startup to file instead of stdout to avoid protocol interference
74
+ Mcpeasy::Config.ensure_config_dirs
75
+ File.write(Mcpeasy::Config.log_file_path("gdrive", "startup"), "#{Time.now}: Google Drive MCP Server starting on stdio\n", mode: "a")
76
+ while (line = $stdin.gets)
77
+ handle_request(line.strip)
78
+ end
79
+ rescue Interrupt
80
+ # Silent shutdown
81
+ rescue => e
82
+ # Log to a file instead of stderr to avoid protocol interference
83
+ File.write(Mcpeasy::Config.log_file_path("gdrive", "error"), "#{Time.now}: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
84
+ end
85
+
86
+ private
87
+
88
+ def handle_request(line)
89
+ return if line.empty?
90
+
91
+ begin
92
+ request = JSON.parse(line)
93
+ response = process_request(request)
94
+ if response
95
+ puts JSON.generate(response)
96
+ $stdout.flush
97
+ end
98
+ rescue JSON::ParserError => e
99
+ error_response = {
100
+ jsonrpc: "2.0",
101
+ id: nil,
102
+ error: {
103
+ code: -32700,
104
+ message: "Parse error",
105
+ data: e.message
106
+ }
107
+ }
108
+ puts JSON.generate(error_response)
109
+ $stdout.flush
110
+ rescue => e
111
+ File.write(Mcpeasy::Config.log_file_path("gdrive", "error"), "#{Time.now}: Error handling request: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
112
+ error_response = {
113
+ jsonrpc: "2.0",
114
+ id: request&.dig("id"),
115
+ error: {
116
+ code: -32603,
117
+ message: "Internal error",
118
+ data: e.message
119
+ }
120
+ }
121
+ puts JSON.generate(error_response)
122
+ $stdout.flush
123
+ end
124
+ end
125
+
126
+ def process_request(request)
127
+ id = request["id"]
128
+ method = request["method"]
129
+ params = request["params"] || {}
130
+
131
+ case method
132
+ when "notifications/initialized"
133
+ # Client acknowledgment - no response needed
134
+ nil
135
+ when "initialize"
136
+ initialize_response(id, params)
137
+ when "tools/list"
138
+ tools_list_response(id, params)
139
+ when "tools/call"
140
+ tools_call_response(id, params)
141
+ else
142
+ {
143
+ jsonrpc: "2.0",
144
+ id: id,
145
+ error: {
146
+ code: -32601,
147
+ message: "Method not found",
148
+ data: "Unknown method: #{method}"
149
+ }
150
+ }
151
+ end
152
+ end
153
+
154
+ def initialize_response(id, params)
155
+ {
156
+ jsonrpc: "2.0",
157
+ id: id,
158
+ result: {
159
+ protocolVersion: "2024-11-05",
160
+ capabilities: {
161
+ tools: {}
162
+ },
163
+ serverInfo: {
164
+ name: "gdrive-mcp-server",
165
+ version: "1.0.0"
166
+ }
167
+ }
168
+ }
169
+ end
170
+
171
+ def tools_list_response(id, params)
172
+ {
173
+ jsonrpc: "2.0",
174
+ id: id,
175
+ result: {
176
+ tools: @tools.values
177
+ }
178
+ }
179
+ end
180
+
181
+ def tools_call_response(id, params)
182
+ tool_name = params["name"]
183
+ arguments = params["arguments"] || {}
184
+
185
+ unless @tools.key?(tool_name)
186
+ return {
187
+ jsonrpc: "2.0",
188
+ id: id,
189
+ error: {
190
+ code: -32602,
191
+ message: "Unknown tool",
192
+ data: "Tool '#{tool_name}' not found"
193
+ }
194
+ }
195
+ end
196
+
197
+ begin
198
+ result = call_tool(tool_name, arguments)
199
+ {
200
+ jsonrpc: "2.0",
201
+ id: id,
202
+ result: {
203
+ content: [
204
+ {
205
+ type: "text",
206
+ text: result
207
+ }
208
+ ],
209
+ isError: false
210
+ }
211
+ }
212
+ rescue => e
213
+ File.write(Mcpeasy::Config.log_file_path("gdrive", "error"), "#{Time.now}: Tool error: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
214
+ {
215
+ jsonrpc: "2.0",
216
+ id: id,
217
+ result: {
218
+ content: [
219
+ {
220
+ type: "text",
221
+ text: "❌ Error: #{e.message}"
222
+ }
223
+ ],
224
+ isError: true
225
+ }
226
+ }
227
+ end
228
+ end
229
+
230
+ def call_tool(tool_name, arguments)
231
+ # Initialize GdriveTool only when needed
232
+ @gdrive_tool ||= GdriveTool.new
233
+
234
+ case tool_name
235
+ when "test_connection"
236
+ test_connection
237
+ when "search_files"
238
+ search_files(arguments)
239
+ when "get_file_content"
240
+ get_file_content(arguments)
241
+ when "list_files"
242
+ list_files(arguments)
243
+ else
244
+ raise "Unknown tool: #{tool_name}"
245
+ end
246
+ end
247
+
248
+ def test_connection
249
+ tool = GdriveTool.new
250
+ response = tool.test_connection
251
+ if response[:ok]
252
+ "✅ Successfully connected to Google Drive.\n" \
253
+ " User: #{response[:user]} (#{response[:email]})\n" \
254
+ " Storage: #{format_bytes(response[:storage_used])} / #{format_bytes(response[:storage_limit])}"
255
+ else
256
+ raise "Connection test failed"
257
+ end
258
+ end
259
+
260
+ def search_files(arguments)
261
+ unless arguments["query"]
262
+ raise "Missing required argument: query"
263
+ end
264
+
265
+ query = arguments["query"].to_s
266
+ max_results = arguments["max_results"]&.to_i || 10
267
+
268
+ tool = GdriveTool.new
269
+ result = tool.search_files(query, max_results: max_results)
270
+ files = result[:files]
271
+
272
+ if files.empty?
273
+ "🔍 No files found matching '#{query}'"
274
+ else
275
+ output = "🔍 Found #{result[:count]} file(s) matching '#{query}':\n\n"
276
+ files.each_with_index do |file, index|
277
+ output << "#{index + 1}. **#{file[:name]}**\n"
278
+ output << " - ID: `#{file[:id]}`\n"
279
+ output << " - Type: #{file[:mime_type]}\n"
280
+ output << " - Size: #{format_bytes(file[:size])}\n"
281
+ output << " - Modified: #{file[:modified_time]}\n"
282
+ output << " - Link: #{file[:web_view_link]}\n\n"
283
+ end
284
+ output
285
+ end
286
+ end
287
+
288
+ def get_file_content(arguments)
289
+ unless arguments["file_id"]
290
+ raise "Missing required argument: file_id"
291
+ end
292
+
293
+ file_id = arguments["file_id"].to_s
294
+ tool = GdriveTool.new
295
+ result = tool.get_file_content(file_id)
296
+
297
+ output = "📄 **#{result[:name]}**\n"
298
+ output << " - Type: #{result[:mime_type]}\n"
299
+ output << " - Size: #{format_bytes(result[:size])}\n\n"
300
+ output << "**Content:**\n"
301
+ output << "```\n#{result[:content]}\n```"
302
+ output
303
+ end
304
+
305
+ def list_files(arguments)
306
+ max_results = arguments["max_results"]&.to_i || 20
307
+ tool = GdriveTool.new
308
+ result = tool.list_files(max_results: max_results)
309
+ files = result[:files]
310
+
311
+ if files.empty?
312
+ "📂 No files found in Google Drive"
313
+ else
314
+ output = "📂 Recent #{result[:count]} file(s):\n\n"
315
+ files.each_with_index do |file, index|
316
+ output << "#{index + 1}. **#{file[:name]}**\n"
317
+ output << " - ID: `#{file[:id]}`\n"
318
+ output << " - Type: #{file[:mime_type]}\n"
319
+ output << " - Size: #{format_bytes(file[:size])}\n"
320
+ output << " - Modified: #{file[:modified_time]}\n"
321
+ output << " - Link: #{file[:web_view_link]}\n\n"
322
+ end
323
+ output
324
+ end
325
+ end
326
+
327
+ private
328
+
329
+ def format_bytes(bytes)
330
+ return "Unknown" unless bytes
331
+
332
+ units = %w[B KB MB GB TB]
333
+ size = bytes.to_f
334
+ unit_index = 0
335
+
336
+ while size >= 1024 && unit_index < units.length - 1
337
+ size /= 1024
338
+ unit_index += 1
339
+ end
340
+
341
+ "#{size.round(1)} #{units[unit_index]}"
342
+ end
343
+ end
344
+
345
+ if __FILE__ == $0
346
+ MCPServer.new.run
347
+ end
@@ -0,0 +1,133 @@
1
+ # Google Meet MCP Server
2
+
3
+ A Model Context Protocol (MCP) server for Google Meet integration. This tool allows you to list, search, and get direct links to Google Meet meetings from your Google Calendar.
4
+
5
+ ## Features
6
+
7
+ - **List Google Meet meetings** with date filtering
8
+ - **Search for meetings** by title or description
9
+ - **Get upcoming meetings** in the next 24 hours
10
+ - **Extract Google Meet URLs** for direct browser access
11
+ - **CLI and MCP server modes** for flexible usage
12
+
13
+ ## Setup
14
+
15
+ ### 1. Google API Credentials
16
+
17
+ 1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
18
+ 2. Create a new project or select an existing one
19
+ 3. Enable the Google Calendar API
20
+ 4. Create credentials (OAuth 2.0 Client ID) for a "Desktop application"
21
+ 5. Download the credentials and note the Client ID and Client Secret
22
+
23
+ ### 2. OAuth Credentials Setup
24
+
25
+ The Google OAuth credentials will be configured during the authentication process. You'll need the Client ID and Client Secret from your Google Cloud Console setup.
26
+
27
+ ### 3. Install Dependencies
28
+
29
+ ```bash
30
+ bundle install
31
+ ```
32
+
33
+ ### 4. Authentication
34
+
35
+ Run the authentication flow to get access to your Google Calendar:
36
+
37
+ ```bash
38
+ mcpz google auth
39
+ ```
40
+
41
+ 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.
42
+
43
+ ## Usage
44
+
45
+ ### CLI Mode
46
+
47
+ **Test connection:**
48
+ ```bash
49
+ mcpz gmeet test
50
+ ```
51
+
52
+ **List Google Meet meetings:**
53
+ ```bash
54
+ mcpz gmeet meetings
55
+ mcpz gmeet meetings --start_date 2024-01-01 --end_date 2024-01-07
56
+ mcpz gmeet meetings --max_results 10
57
+ ```
58
+
59
+ **List upcoming meetings:**
60
+ ```bash
61
+ mcpz gmeet upcoming
62
+ mcpz gmeet upcoming --max_results 5
63
+ ```
64
+
65
+ **Search for meetings:**
66
+ ```bash
67
+ mcpz gmeet search "standup"
68
+ mcpz gmeet search "team meeting" --start_date 2024-01-01
69
+ ```
70
+
71
+ **Get meeting URL by event ID:**
72
+ ```bash
73
+ mcpz gmeet url event_id_here
74
+ ```
75
+
76
+ ### MCP Server Mode
77
+
78
+ Configure in your `.mcp.json`:
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "gmeet": {
84
+ "command": "mcpz",
85
+ "args": ["gmeet", "mcp"]
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ Available MCP tools:
92
+ - `test_connection` - Test Google Calendar API connection
93
+ - `list_meetings` - List Google Meet meetings with date filtering
94
+ - `upcoming_meetings` - List upcoming meetings in next 24 hours
95
+ - `search_meetings` - Search meetings by text content
96
+ - `get_meeting_url` - Get Google Meet URL for specific event
97
+
98
+ ## How It Works
99
+
100
+ The tool uses the Google Calendar API to:
101
+
102
+ 1. **Fetch calendar events** from your Google Calendar
103
+ 2. **Filter for Google Meet meetings** by detecting:
104
+ - Conference data with Google Meet
105
+ - Hangout links (legacy)
106
+ - Meet.google.com URLs in descriptions
107
+ - Meet.google.com URLs in location fields
108
+ 3. **Extract meeting URLs** for direct browser access
109
+ 4. **Format and present** meeting information
110
+
111
+ ## Troubleshooting
112
+
113
+ **Authentication Issues:**
114
+ - Re-run the auth flow: `mcpz google auth`
115
+ - Check that the Google Calendar API is enabled in your Google Cloud project
116
+ - Verify your Google OAuth credentials are configured correctly
117
+
118
+ **No meetings found:**
119
+ - Verify you have Google Meet meetings in your calendar
120
+ - Check the date range (default is 7 days from today)
121
+ - Ensure meetings have Google Meet links attached
122
+
123
+ **MCP Server Issues:**
124
+ - Check logs in `./logs/mcp_gmeet_error.log`
125
+ - Verify the server path in `.mcp.json` is correct
126
+ - Ensure all dependencies are installed with `bundle install`
127
+
128
+ ## API Permissions
129
+
130
+ This tool requires:
131
+ - `https://www.googleapis.com/auth/calendar.readonly` - Read access to your Google Calendar
132
+
133
+ The tool only reads calendar data and never modifies your calendar or meetings.
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "thor"
5
+ require_relative "gmeet_tool"
6
+
7
+ class GmeetCLI < 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}"
20
+ exit 1
21
+ end
22
+
23
+ desc "meetings", "List Google Meet meetings"
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 meetings"
27
+ method_option :calendar_id, type: :string, aliases: "-c", desc: "Calendar ID (default: primary)"
28
+ def meetings
29
+ result = tool.list_meetings(
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
+ meetings = result[:meetings]
36
+
37
+ if meetings.empty?
38
+ puts "🎥 No Google Meet meetings found for the specified date range"
39
+ else
40
+ puts "🎥 Found #{result[:count]} Google Meet meeting(s):"
41
+ meetings.each_with_index do |meeting, index|
42
+ puts " #{index + 1}. #{meeting[:summary] || "No title"}"
43
+ puts " Start: #{format_datetime(meeting[:start])}"
44
+ puts " End: #{format_datetime(meeting[:end])}"
45
+ puts " Description: #{meeting[:description]}" if meeting[:description]
46
+ puts " Location: #{meeting[:location]}" if meeting[:location]
47
+ puts " Attendees: #{meeting[:attendees].join(", ")}" if meeting[:attendees]&.any?
48
+ puts " Meet Link: #{meeting[:meet_link]}"
49
+ puts " Calendar Link: #{meeting[:html_link]}"
50
+ puts
51
+ end
52
+ end
53
+ rescue RuntimeError => e
54
+ warn "❌ Failed to list meetings: #{e.message}"
55
+ exit 1
56
+ end
57
+
58
+ desc "upcoming", "List upcoming Google Meet meetings"
59
+ method_option :max_results, type: :numeric, default: 10, aliases: "-n", desc: "Max number of meetings"
60
+ method_option :calendar_id, type: :string, aliases: "-c", desc: "Calendar ID (default: primary)"
61
+ def upcoming
62
+ result = tool.upcoming_meetings(
63
+ max_results: options[:max_results],
64
+ calendar_id: options[:calendar_id] || "primary"
65
+ )
66
+ meetings = result[:meetings]
67
+
68
+ if meetings.empty?
69
+ puts "🎥 No upcoming Google Meet meetings found in the next 24 hours"
70
+ else
71
+ puts "🎥 Found #{result[:count]} upcoming Google Meet meeting(s):"
72
+ meetings.each_with_index do |meeting, index|
73
+ puts " #{index + 1}. #{meeting[:summary] || "No title"}"
74
+ puts " Start: #{format_datetime(meeting[:start])} (#{meeting[:time_until_start]})"
75
+ puts " End: #{format_datetime(meeting[:end])}"
76
+ puts " Description: #{meeting[:description]}" if meeting[:description]
77
+ puts " Location: #{meeting[:location]}" if meeting[:location]
78
+ puts " Attendees: #{meeting[:attendees].join(", ")}" if meeting[:attendees]&.any?
79
+ puts " Meet Link: #{meeting[:meet_link]}"
80
+ puts " Calendar Link: #{meeting[:html_link]}"
81
+ puts
82
+ end
83
+ end
84
+ rescue RuntimeError => e
85
+ warn "❌ Failed to list upcoming meetings: #{e.message}"
86
+ exit 1
87
+ end
88
+
89
+ desc "search QUERY", "Search for Google Meet meetings by text content"
90
+ method_option :start_date, type: :string, aliases: "-s", desc: "Start date (YYYY-MM-DD)"
91
+ method_option :end_date, type: :string, aliases: "-e", desc: "End date (YYYY-MM-DD)"
92
+ method_option :max_results, type: :numeric, default: 10, aliases: "-n", desc: "Max number of meetings"
93
+ def search(query)
94
+ result = tool.search_meetings(
95
+ query,
96
+ start_date: options[:start_date],
97
+ end_date: options[:end_date],
98
+ max_results: options[:max_results]
99
+ )
100
+ meetings = result[:meetings]
101
+
102
+ if meetings.empty?
103
+ puts "🔍 No Google Meet meetings found matching '#{query}'"
104
+ else
105
+ puts "🔍 Found #{result[:count]} Google Meet meeting(s) matching '#{query}':"
106
+ meetings.each_with_index do |meeting, index|
107
+ puts " #{index + 1}. #{meeting[:summary] || "No title"}"
108
+ puts " Start: #{format_datetime(meeting[:start])}"
109
+ puts " End: #{format_datetime(meeting[:end])}"
110
+ puts " Description: #{meeting[:description]}" if meeting[:description]
111
+ puts " Location: #{meeting[:location]}" if meeting[:location]
112
+ puts " Meet Link: #{meeting[:meet_link]}"
113
+ puts " Calendar Link: #{meeting[:html_link]}"
114
+ puts
115
+ end
116
+ end
117
+ rescue RuntimeError => e
118
+ warn "❌ Failed to search meetings: #{e.message}"
119
+ exit 1
120
+ end
121
+
122
+ desc "url EVENT_ID", "Get the Google Meet URL for a specific event"
123
+ method_option :calendar_id, type: :string, aliases: "-c", desc: "Calendar ID (default: primary)"
124
+ def url(event_id)
125
+ result = tool.get_meeting_url(event_id, calendar_id: options[:calendar_id] || "primary")
126
+
127
+ puts "🎥 #{result[:summary] || "Meeting"}"
128
+ puts " Start: #{format_datetime(result[:start])}"
129
+ puts " End: #{format_datetime(result[:end])}"
130
+ puts " Meet Link: #{result[:meet_link]}"
131
+ puts " Event ID: #{result[:event_id]}"
132
+ rescue RuntimeError => e
133
+ warn "❌ Failed to get meeting URL: #{e.message}"
134
+ exit 1
135
+ end
136
+
137
+ private
138
+
139
+ def tool
140
+ @tool ||= GmeetTool.new
141
+ end
142
+
143
+ def format_datetime(datetime_info)
144
+ return "Unknown" unless datetime_info
145
+
146
+ if datetime_info[:date]
147
+ # All-day event
148
+ datetime_info[:date]
149
+ elsif datetime_info[:date_time]
150
+ # Specific time event
151
+ time = Time.parse(datetime_info[:date_time])
152
+ time.strftime("%Y-%m-%d %H:%M")
153
+ else
154
+ "Unknown"
155
+ end
156
+ end
157
+ end