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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.claudeignore +0 -3
  3. data/.mcp.json +19 -1
  4. data/CHANGELOG.md +59 -0
  5. data/CLAUDE.md +19 -5
  6. data/README.md +19 -3
  7. data/lib/mcpeasy/cli.rb +62 -10
  8. data/lib/mcpeasy/config.rb +22 -1
  9. data/lib/mcpeasy/setup.rb +1 -0
  10. data/lib/mcpeasy/version.rb +1 -1
  11. data/lib/utilities/gcal/README.md +11 -3
  12. data/lib/utilities/gcal/cli.rb +110 -108
  13. data/lib/utilities/gcal/mcp.rb +463 -308
  14. data/lib/utilities/gcal/service.rb +312 -0
  15. data/lib/utilities/gdrive/README.md +3 -3
  16. data/lib/utilities/gdrive/cli.rb +98 -96
  17. data/lib/utilities/gdrive/mcp.rb +290 -288
  18. data/lib/utilities/gdrive/service.rb +293 -0
  19. data/lib/utilities/gmail/README.md +278 -0
  20. data/lib/utilities/gmail/cli.rb +264 -0
  21. data/lib/utilities/gmail/mcp.rb +846 -0
  22. data/lib/utilities/gmail/service.rb +547 -0
  23. data/lib/utilities/gmeet/cli.rb +131 -129
  24. data/lib/utilities/gmeet/mcp.rb +374 -372
  25. data/lib/utilities/gmeet/service.rb +411 -0
  26. data/lib/utilities/notion/README.md +287 -0
  27. data/lib/utilities/notion/cli.rb +245 -0
  28. data/lib/utilities/notion/mcp.rb +607 -0
  29. data/lib/utilities/notion/service.rb +327 -0
  30. data/lib/utilities/slack/README.md +3 -3
  31. data/lib/utilities/slack/cli.rb +69 -54
  32. data/lib/utilities/slack/mcp.rb +277 -226
  33. data/lib/utilities/slack/service.rb +134 -0
  34. data/mcpeasy.gemspec +6 -1
  35. metadata +87 -10
  36. data/env.template +0 -11
  37. data/lib/utilities/gcal/gcal_tool.rb +0 -308
  38. data/lib/utilities/gdrive/gdrive_tool.rb +0 -291
  39. data/lib/utilities/gmeet/gmeet_tool.rb +0 -407
  40. data/lib/utilities/slack/slack_tool.rb +0 -119
  41. data/logs/.keep +0 -0
@@ -3,345 +3,347 @@
3
3
 
4
4
  require "bundler/setup"
5
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"
6
+ require_relative "service"
7
+
8
+ module Gdrive
9
+ class MCPServer
10
+ def initialize
11
+ @tools = {
12
+ "test_connection" => {
13
+ name: "test_connection",
14
+ description: "Test the Google Drive API connection",
15
+ inputSchema: {
16
+ type: "object",
17
+ properties: {},
18
+ required: []
19
+ }
20
+ },
21
+ "search_files" => {
22
+ name: "search_files",
23
+ description: "Search for files in Google Drive by content or name",
24
+ inputSchema: {
25
+ type: "object",
26
+ properties: {
27
+ query: {
28
+ type: "string",
29
+ description: "Search query to find files"
30
+ },
31
+ max_results: {
32
+ type: "number",
33
+ description: "Maximum number of results to return (default: 10)"
34
+ }
29
35
  },
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: []
36
+ required: ["query"]
37
+ }
38
+ },
39
+ "get_file_content" => {
40
+ name: "get_file_content",
41
+ description: "Get the content of a specific Google Drive file",
42
+ inputSchema: {
43
+ type: "object",
44
+ properties: {
45
+ file_id: {
46
+ type: "string",
47
+ description: "The Google Drive file ID"
48
+ }
49
+ },
50
+ required: ["file_id"]
51
+ }
52
+ },
53
+ "list_files" => {
54
+ name: "list_files",
55
+ description: "List recent files in Google Drive",
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ max_results: {
60
+ type: "number",
61
+ description: "Maximum number of files to return (default: 20)"
62
+ }
63
+ },
64
+ required: []
65
+ }
64
66
  }
65
67
  }
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
68
  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
69
 
86
- private
70
+ def run
71
+ # Disable stdout buffering for immediate response
72
+ $stdout.sync = true
87
73
 
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
74
+ # Log startup to file instead of stdout to avoid protocol interference
75
+ Mcpeasy::Config.ensure_config_dirs
76
+ File.write(Mcpeasy::Config.log_file_path("gdrive", "startup"), "#{Time.now}: Google Drive MCP Server starting on stdio\n", mode: "a")
77
+ while (line = $stdin.gets)
78
+ handle_request(line.strip)
97
79
  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
80
+ rescue Interrupt
81
+ # Silent shutdown
110
82
  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
83
+ # Log to a file instead of stderr to avoid protocol interference
84
+ File.write(Mcpeasy::Config.log_file_path("gdrive", "error"), "#{Time.now}: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
123
85
  end
124
- end
125
86
 
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}"
87
+ private
88
+
89
+ def handle_request(line)
90
+ return if line.empty?
91
+
92
+ begin
93
+ request = JSON.parse(line)
94
+ response = process_request(request)
95
+ if response
96
+ puts JSON.generate(response)
97
+ $stdout.flush
98
+ end
99
+ rescue JSON::ParserError => e
100
+ error_response = {
101
+ jsonrpc: "2.0",
102
+ id: nil,
103
+ error: {
104
+ code: -32700,
105
+ message: "Parse error",
106
+ data: e.message
107
+ }
149
108
  }
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"
109
+ puts JSON.generate(error_response)
110
+ $stdout.flush
111
+ rescue => e
112
+ File.write(Mcpeasy::Config.log_file_path("gdrive", "error"), "#{Time.now}: Error handling request: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
113
+ error_response = {
114
+ jsonrpc: "2.0",
115
+ id: request&.dig("id"),
116
+ error: {
117
+ code: -32603,
118
+ message: "Internal error",
119
+ data: e.message
120
+ }
166
121
  }
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"] || {}
122
+ puts JSON.generate(error_response)
123
+ $stdout.flush
124
+ end
125
+ end
184
126
 
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"
127
+ def process_request(request)
128
+ id = request["id"]
129
+ method = request["method"]
130
+ params = request["params"] || {}
131
+
132
+ case method
133
+ when "notifications/initialized"
134
+ # Client acknowledgment - no response needed
135
+ nil
136
+ when "initialize"
137
+ initialize_response(id, params)
138
+ when "tools/list"
139
+ tools_list_response(id, params)
140
+ when "tools/call"
141
+ tools_call_response(id, params)
142
+ else
143
+ {
144
+ jsonrpc: "2.0",
145
+ id: id,
146
+ error: {
147
+ code: -32601,
148
+ message: "Method not found",
149
+ data: "Unknown method: #{method}"
150
+ }
193
151
  }
194
- }
152
+ end
195
153
  end
196
154
 
197
- begin
198
- result = call_tool(tool_name, arguments)
155
+ def initialize_response(id, params)
199
156
  {
200
157
  jsonrpc: "2.0",
201
158
  id: id,
202
159
  result: {
203
- content: [
204
- {
205
- type: "text",
206
- text: result
207
- }
208
- ],
209
- isError: false
160
+ protocolVersion: "2024-11-05",
161
+ capabilities: {
162
+ tools: {}
163
+ },
164
+ serverInfo: {
165
+ name: "gdrive-mcp-server",
166
+ version: "1.0.0"
167
+ }
210
168
  }
211
169
  }
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")
170
+ end
171
+
172
+ def tools_list_response(id, params)
214
173
  {
215
174
  jsonrpc: "2.0",
216
175
  id: id,
217
176
  result: {
218
- content: [
219
- {
220
- type: "text",
221
- text: "❌ Error: #{e.message}"
222
- }
223
- ],
224
- isError: true
177
+ tools: @tools.values
225
178
  }
226
179
  }
227
180
  end
228
- end
229
181
 
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
182
+ def tools_call_response(id, params)
183
+ tool_name = params["name"]
184
+ arguments = params["arguments"] || {}
185
+
186
+ unless @tools.key?(tool_name)
187
+ return {
188
+ jsonrpc: "2.0",
189
+ id: id,
190
+ error: {
191
+ code: -32602,
192
+ message: "Unknown tool",
193
+ data: "Tool '#{tool_name}' not found"
194
+ }
195
+ }
196
+ end
247
197
 
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"
198
+ begin
199
+ result = call_tool(tool_name, arguments)
200
+ {
201
+ jsonrpc: "2.0",
202
+ id: id,
203
+ result: {
204
+ content: [
205
+ {
206
+ type: "text",
207
+ text: result
208
+ }
209
+ ],
210
+ isError: false
211
+ }
212
+ }
213
+ rescue => e
214
+ File.write(Mcpeasy::Config.log_file_path("gdrive", "error"), "#{Time.now}: Tool error: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
215
+ {
216
+ jsonrpc: "2.0",
217
+ id: id,
218
+ result: {
219
+ content: [
220
+ {
221
+ type: "text",
222
+ text: "❌ Error: #{e.message}"
223
+ }
224
+ ],
225
+ isError: true
226
+ }
227
+ }
228
+ end
257
229
  end
258
- end
259
230
 
260
- def search_files(arguments)
261
- unless arguments["query"]
262
- raise "Missing required argument: query"
231
+ def call_tool(tool_name, arguments)
232
+ # Initialize Service only when needed
233
+ @gdrive_tool ||= Service.new
234
+
235
+ case tool_name
236
+ when "test_connection"
237
+ test_connection
238
+ when "search_files"
239
+ search_files(arguments)
240
+ when "get_file_content"
241
+ get_file_content(arguments)
242
+ when "list_files"
243
+ list_files(arguments)
244
+ else
245
+ raise "Unknown tool: #{tool_name}"
246
+ end
263
247
  end
264
248
 
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"
249
+ def test_connection
250
+ tool = Service.new
251
+ response = tool.test_connection
252
+ if response[:ok]
253
+ "✅ Successfully connected to Google Drive.\n" \
254
+ " User: #{response[:user]} (#{response[:email]})\n" \
255
+ " Storage: #{format_bytes(response[:storage_used])} / #{format_bytes(response[:storage_limit])}"
256
+ else
257
+ raise "Connection test failed"
283
258
  end
284
- output
285
259
  end
286
- end
287
260
 
288
- def get_file_content(arguments)
289
- unless arguments["file_id"]
290
- raise "Missing required argument: file_id"
261
+ def search_files(arguments)
262
+ unless arguments["query"]
263
+ raise "Missing required argument: query"
264
+ end
265
+
266
+ query = arguments["query"].to_s
267
+ max_results = arguments["max_results"]&.to_i || 10
268
+
269
+ tool = Service.new
270
+ result = tool.search_files(query, max_results: max_results)
271
+ files = result[:files]
272
+
273
+ if files.empty?
274
+ "🔍 No files found matching '#{query}'"
275
+ else
276
+ output = "🔍 Found #{result[:count]} file(s) matching '#{query}':\n\n"
277
+ files.each_with_index do |file, index|
278
+ output << "#{index + 1}. **#{file[:name]}**\n"
279
+ output << " - ID: `#{file[:id]}`\n"
280
+ output << " - Type: #{file[:mime_type]}\n"
281
+ output << " - Size: #{format_bytes(file[:size])}\n"
282
+ output << " - Modified: #{file[:modified_time]}\n"
283
+ output << " - Link: #{file[:web_view_link]}\n\n"
284
+ end
285
+ output
286
+ end
291
287
  end
292
288
 
293
- file_id = arguments["file_id"].to_s
294
- tool = GdriveTool.new
295
- result = tool.get_file_content(file_id)
289
+ def get_file_content(arguments)
290
+ unless arguments["file_id"]
291
+ raise "Missing required argument: file_id"
292
+ end
293
+
294
+ file_id = arguments["file_id"].to_s
295
+ tool = Service.new
296
+ result = tool.get_file_content(file_id)
296
297
 
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
298
+ output = "📄 **#{result[:name]}**\n"
299
+ output << " - Type: #{result[:mime_type]}\n"
300
+ output << " - Size: #{format_bytes(result[:size])}\n\n"
301
+ output << "**Content:**\n"
302
+ output << "```\n#{result[:content]}\n```"
303
+ output
304
+ end
304
305
 
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"
306
+ def list_files(arguments)
307
+ max_results = arguments["max_results"]&.to_i || 20
308
+ tool = Service.new
309
+ result = tool.list_files(max_results: max_results)
310
+ files = result[:files]
311
+
312
+ if files.empty?
313
+ "📂 No files found in Google Drive"
314
+ else
315
+ output = "📂 Recent #{result[:count]} file(s):\n\n"
316
+ files.each_with_index do |file, index|
317
+ output << "#{index + 1}. **#{file[:name]}**\n"
318
+ output << " - ID: `#{file[:id]}`\n"
319
+ output << " - Type: #{file[:mime_type]}\n"
320
+ output << " - Size: #{format_bytes(file[:size])}\n"
321
+ output << " - Modified: #{file[:modified_time]}\n"
322
+ output << " - Link: #{file[:web_view_link]}\n\n"
323
+ end
324
+ output
322
325
  end
323
- output
324
326
  end
325
- end
326
327
 
327
- private
328
+ private
328
329
 
329
- def format_bytes(bytes)
330
- return "Unknown" unless bytes
330
+ def format_bytes(bytes)
331
+ return "Unknown" unless bytes
331
332
 
332
- units = %w[B KB MB GB TB]
333
- size = bytes.to_f
334
- unit_index = 0
333
+ units = %w[B KB MB GB TB]
334
+ size = bytes.to_f
335
+ unit_index = 0
335
336
 
336
- while size >= 1024 && unit_index < units.length - 1
337
- size /= 1024
338
- unit_index += 1
339
- end
337
+ while size >= 1024 && unit_index < units.length - 1
338
+ size /= 1024
339
+ unit_index += 1
340
+ end
340
341
 
341
- "#{size.round(1)} #{units[unit_index]}"
342
+ "#{size.round(1)} #{units[unit_index]}"
343
+ end
342
344
  end
343
345
  end
344
346
 
345
347
  if __FILE__ == $0
346
- MCPServer.new.run
348
+ Gdrive::MCPServer.new.run
347
349
  end