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.
- checksums.yaml +4 -4
- data/.claudeignore +0 -3
- data/.mcp.json +19 -1
- data/CHANGELOG.md +59 -0
- data/CLAUDE.md +19 -5
- data/README.md +19 -3
- data/lib/mcpeasy/cli.rb +62 -10
- data/lib/mcpeasy/config.rb +22 -1
- data/lib/mcpeasy/setup.rb +1 -0
- data/lib/mcpeasy/version.rb +1 -1
- data/lib/utilities/gcal/README.md +11 -3
- data/lib/utilities/gcal/cli.rb +110 -108
- data/lib/utilities/gcal/mcp.rb +463 -308
- data/lib/utilities/gcal/service.rb +312 -0
- data/lib/utilities/gdrive/README.md +3 -3
- data/lib/utilities/gdrive/cli.rb +98 -96
- data/lib/utilities/gdrive/mcp.rb +290 -288
- data/lib/utilities/gdrive/service.rb +293 -0
- data/lib/utilities/gmail/README.md +278 -0
- data/lib/utilities/gmail/cli.rb +264 -0
- data/lib/utilities/gmail/mcp.rb +846 -0
- data/lib/utilities/gmail/service.rb +547 -0
- data/lib/utilities/gmeet/cli.rb +131 -129
- data/lib/utilities/gmeet/mcp.rb +374 -372
- data/lib/utilities/gmeet/service.rb +411 -0
- data/lib/utilities/notion/README.md +287 -0
- data/lib/utilities/notion/cli.rb +245 -0
- data/lib/utilities/notion/mcp.rb +607 -0
- data/lib/utilities/notion/service.rb +327 -0
- data/lib/utilities/slack/README.md +3 -3
- data/lib/utilities/slack/cli.rb +69 -54
- data/lib/utilities/slack/mcp.rb +277 -226
- data/lib/utilities/slack/service.rb +134 -0
- data/mcpeasy.gemspec +6 -1
- metadata +87 -10
- data/env.template +0 -11
- data/lib/utilities/gcal/gcal_tool.rb +0 -308
- data/lib/utilities/gdrive/gdrive_tool.rb +0 -291
- data/lib/utilities/gmeet/gmeet_tool.rb +0 -407
- data/lib/utilities/slack/slack_tool.rb +0 -119
- data/logs/.keep +0 -0
data/lib/utilities/gdrive/mcp.rb
CHANGED
@@ -3,345 +3,347 @@
|
|
3
3
|
|
4
4
|
require "bundler/setup"
|
5
5
|
require "json"
|
6
|
-
require_relative "
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
70
|
+
def run
|
71
|
+
# Disable stdout buffering for immediate response
|
72
|
+
$stdout.sync = true
|
87
73
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
99
|
-
|
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
|
-
|
112
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
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
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
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
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
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
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
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
|
-
|
213
|
-
|
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
|
-
|
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
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
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
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
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
|
-
|
261
|
-
|
262
|
-
|
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
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
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
|
-
|
289
|
-
|
290
|
-
|
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
|
-
|
294
|
-
|
295
|
-
|
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
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
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
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
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
|
-
|
328
|
+
private
|
328
329
|
|
329
|
-
|
330
|
-
|
330
|
+
def format_bytes(bytes)
|
331
|
+
return "Unknown" unless bytes
|
331
332
|
|
332
|
-
|
333
|
-
|
334
|
-
|
333
|
+
units = %w[B KB MB GB TB]
|
334
|
+
size = bytes.to_f
|
335
|
+
unit_index = 0
|
335
336
|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
337
|
+
while size >= 1024 && unit_index < units.length - 1
|
338
|
+
size /= 1024
|
339
|
+
unit_index += 1
|
340
|
+
end
|
340
341
|
|
341
|
-
|
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
|