mcpeasy 0.1.0 → 0.2.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 +10 -1
- data/CHANGELOG.md +50 -0
- data/CLAUDE.md +19 -5
- data/README.md +19 -3
- data/lib/mcpeasy/cli.rb +33 -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/gmeet/cli.rb +131 -129
- data/lib/utilities/gmeet/mcp.rb +374 -372
- data/lib/utilities/gmeet/service.rb +409 -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
- metadata +11 -8
- 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/slack/mcp.rb
CHANGED
@@ -3,278 +3,329 @@
|
|
3
3
|
|
4
4
|
require "bundler/setup"
|
5
5
|
require "json"
|
6
|
-
require_relative "
|
6
|
+
require_relative "service"
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
text: {
|
42
|
-
type: "string",
|
43
|
-
description: "The message text to post"
|
8
|
+
module Slack
|
9
|
+
class MCPServer
|
10
|
+
def initialize
|
11
|
+
# Defer SlackTool initialization until actually needed
|
12
|
+
@slack_tool = nil
|
13
|
+
@tools = {
|
14
|
+
"test_connection" => {
|
15
|
+
name: "test_connection",
|
16
|
+
description: "Test the Slack API connection",
|
17
|
+
inputSchema: {
|
18
|
+
type: "object",
|
19
|
+
properties: {},
|
20
|
+
required: []
|
21
|
+
}
|
22
|
+
},
|
23
|
+
"list_channels" => {
|
24
|
+
name: "list_channels",
|
25
|
+
description: "List available Slack channels. When asked to list ALL channels, automatically retrieve all pages by calling this tool multiple times with the cursor parameter to get complete results.",
|
26
|
+
inputSchema: {
|
27
|
+
type: "object",
|
28
|
+
properties: {
|
29
|
+
limit: {
|
30
|
+
type: "number",
|
31
|
+
description: "Maximum number of channels to return (default: 100, max: 1000)"
|
32
|
+
},
|
33
|
+
cursor: {
|
34
|
+
type: "string",
|
35
|
+
description: "Cursor for pagination. Use the next_cursor from previous response to get next page"
|
36
|
+
},
|
37
|
+
exclude_archived: {
|
38
|
+
type: "boolean",
|
39
|
+
description: "Exclude archived channels from results (default: true)"
|
40
|
+
}
|
44
41
|
},
|
45
|
-
|
46
|
-
|
47
|
-
|
42
|
+
required: []
|
43
|
+
}
|
44
|
+
},
|
45
|
+
"post_message" => {
|
46
|
+
name: "post_message",
|
47
|
+
description: "Post a message to a Slack channel",
|
48
|
+
inputSchema: {
|
49
|
+
type: "object",
|
50
|
+
properties: {
|
51
|
+
channel: {
|
52
|
+
type: "string",
|
53
|
+
description: "The Slack channel name (with or without #)"
|
54
|
+
},
|
55
|
+
text: {
|
56
|
+
type: "string",
|
57
|
+
description: "The message text to post"
|
58
|
+
},
|
59
|
+
username: {
|
60
|
+
type: "string",
|
61
|
+
description: "Optional custom username for the message"
|
62
|
+
},
|
63
|
+
thread_ts: {
|
64
|
+
type: "string",
|
65
|
+
description: "Optional timestamp of parent message to reply to"
|
66
|
+
}
|
48
67
|
},
|
49
|
-
|
50
|
-
|
51
|
-
description: "Optional timestamp of parent message to reply to"
|
52
|
-
}
|
53
|
-
},
|
54
|
-
required: ["channel", "text"]
|
68
|
+
required: ["channel", "text"]
|
69
|
+
}
|
55
70
|
}
|
56
71
|
}
|
57
|
-
}
|
58
|
-
end
|
59
|
-
|
60
|
-
def run
|
61
|
-
# Disable stdout buffering for immediate response
|
62
|
-
$stdout.sync = true
|
63
|
-
|
64
|
-
# Log startup to file instead of stdout to avoid protocol interference
|
65
|
-
Mcpeasy::Config.ensure_config_dirs
|
66
|
-
File.write(Mcpeasy::Config.log_file_path("slack", "startup"), "#{Time.now}: Slack MCP Server starting on stdio\n", mode: "a")
|
67
|
-
while (line = $stdin.gets)
|
68
|
-
handle_request(line.strip)
|
69
72
|
end
|
70
|
-
rescue Interrupt
|
71
|
-
# Silent shutdown
|
72
|
-
rescue => e
|
73
|
-
# Log to a file instead of stderr to avoid protocol interference
|
74
|
-
File.write(Mcpeasy::Config.log_file_path("slack", "error"), "#{Time.now}: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
|
75
|
-
end
|
76
73
|
|
77
|
-
|
74
|
+
def run
|
75
|
+
# Disable stdout buffering for immediate response
|
76
|
+
$stdout.sync = true
|
78
77
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
response = process_request(request)
|
85
|
-
if response
|
86
|
-
puts JSON.generate(response)
|
87
|
-
$stdout.flush
|
78
|
+
# Log startup to file instead of stdout to avoid protocol interference
|
79
|
+
Mcpeasy::Config.ensure_config_dirs
|
80
|
+
File.write(Mcpeasy::Config.log_file_path("slack", "startup"), "#{Time.now}: Slack MCP Server starting on stdio\n", mode: "a")
|
81
|
+
while (line = $stdin.gets)
|
82
|
+
handle_request(line.strip)
|
88
83
|
end
|
89
|
-
rescue
|
90
|
-
|
91
|
-
jsonrpc: "2.0",
|
92
|
-
id: nil,
|
93
|
-
error: {
|
94
|
-
code: -32700,
|
95
|
-
message: "Parse error",
|
96
|
-
data: e.message
|
97
|
-
}
|
98
|
-
}
|
99
|
-
puts JSON.generate(error_response)
|
100
|
-
$stdout.flush
|
84
|
+
rescue Interrupt
|
85
|
+
# Silent shutdown
|
101
86
|
rescue => e
|
102
|
-
|
103
|
-
|
104
|
-
jsonrpc: "2.0",
|
105
|
-
id: request&.dig("id"),
|
106
|
-
error: {
|
107
|
-
code: -32603,
|
108
|
-
message: "Internal error",
|
109
|
-
data: e.message
|
110
|
-
}
|
111
|
-
}
|
112
|
-
puts JSON.generate(error_response)
|
113
|
-
$stdout.flush
|
87
|
+
# Log to a file instead of stderr to avoid protocol interference
|
88
|
+
File.write(Mcpeasy::Config.log_file_path("slack", "error"), "#{Time.now}: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
|
114
89
|
end
|
115
|
-
end
|
116
90
|
|
117
|
-
|
118
|
-
id = request["id"]
|
119
|
-
method = request["method"]
|
120
|
-
params = request["params"] || {}
|
91
|
+
private
|
121
92
|
|
122
|
-
|
123
|
-
|
124
|
-
# Client acknowledgment - no response needed
|
125
|
-
nil
|
126
|
-
when "initialize"
|
127
|
-
initialize_response(id, params)
|
128
|
-
when "tools/list"
|
129
|
-
tools_list_response(id, params)
|
130
|
-
when "tools/call"
|
131
|
-
tools_call_response(id, params)
|
132
|
-
else
|
133
|
-
{
|
134
|
-
jsonrpc: "2.0",
|
135
|
-
id: id,
|
136
|
-
error: {
|
137
|
-
code: -32601,
|
138
|
-
message: "Method not found",
|
139
|
-
data: "Unknown method: #{method}"
|
140
|
-
}
|
141
|
-
}
|
142
|
-
end
|
143
|
-
end
|
93
|
+
def handle_request(line)
|
94
|
+
return if line.empty?
|
144
95
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
96
|
+
begin
|
97
|
+
request = JSON.parse(line)
|
98
|
+
response = process_request(request)
|
99
|
+
if response
|
100
|
+
puts JSON.generate(response)
|
101
|
+
$stdout.flush
|
102
|
+
end
|
103
|
+
rescue JSON::ParserError => e
|
104
|
+
error_response = {
|
105
|
+
jsonrpc: "2.0",
|
106
|
+
id: nil,
|
107
|
+
error: {
|
108
|
+
code: -32700,
|
109
|
+
message: "Parse error",
|
110
|
+
data: e.message
|
111
|
+
}
|
157
112
|
}
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
113
|
+
puts JSON.generate(error_response)
|
114
|
+
$stdout.flush
|
115
|
+
rescue => e
|
116
|
+
File.write(Mcpeasy::Config.log_file_path("slack", "error"), "#{Time.now}: Error handling request: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
|
117
|
+
error_response = {
|
118
|
+
jsonrpc: "2.0",
|
119
|
+
id: request&.dig("id"),
|
120
|
+
error: {
|
121
|
+
code: -32603,
|
122
|
+
message: "Internal error",
|
123
|
+
data: e.message
|
124
|
+
}
|
125
|
+
}
|
126
|
+
puts JSON.generate(error_response)
|
127
|
+
$stdout.flush
|
128
|
+
end
|
129
|
+
end
|
171
130
|
|
172
|
-
|
173
|
-
|
174
|
-
|
131
|
+
def process_request(request)
|
132
|
+
id = request["id"]
|
133
|
+
method = request["method"]
|
134
|
+
params = request["params"] || {}
|
175
135
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
136
|
+
case method
|
137
|
+
when "notifications/initialized"
|
138
|
+
# Client acknowledgment - no response needed
|
139
|
+
nil
|
140
|
+
when "initialize"
|
141
|
+
initialize_response(id, params)
|
142
|
+
when "tools/list"
|
143
|
+
tools_list_response(id, params)
|
144
|
+
when "tools/call"
|
145
|
+
tools_call_response(id, params)
|
146
|
+
else
|
147
|
+
{
|
148
|
+
jsonrpc: "2.0",
|
149
|
+
id: id,
|
150
|
+
error: {
|
151
|
+
code: -32601,
|
152
|
+
message: "Method not found",
|
153
|
+
data: "Unknown method: #{method}"
|
154
|
+
}
|
184
155
|
}
|
185
|
-
|
156
|
+
end
|
186
157
|
end
|
187
158
|
|
188
|
-
|
189
|
-
result = call_tool(tool_name, arguments)
|
159
|
+
def initialize_response(id, params)
|
190
160
|
{
|
191
161
|
jsonrpc: "2.0",
|
192
162
|
id: id,
|
193
163
|
result: {
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
164
|
+
protocolVersion: "2024-11-05",
|
165
|
+
capabilities: {
|
166
|
+
tools: {}
|
167
|
+
},
|
168
|
+
serverInfo: {
|
169
|
+
name: "slack-mcp-server",
|
170
|
+
version: "1.0.0"
|
171
|
+
}
|
201
172
|
}
|
202
173
|
}
|
203
|
-
|
204
|
-
|
174
|
+
end
|
175
|
+
|
176
|
+
def tools_list_response(id, params)
|
205
177
|
{
|
206
178
|
jsonrpc: "2.0",
|
207
179
|
id: id,
|
208
180
|
result: {
|
209
|
-
|
210
|
-
{
|
211
|
-
type: "text",
|
212
|
-
text: "❌ Error: #{e.message}"
|
213
|
-
}
|
214
|
-
],
|
215
|
-
isError: true
|
181
|
+
tools: @tools.values
|
216
182
|
}
|
217
183
|
}
|
218
184
|
end
|
219
|
-
end
|
220
185
|
|
221
|
-
|
222
|
-
|
223
|
-
|
186
|
+
def tools_call_response(id, params)
|
187
|
+
tool_name = params["name"]
|
188
|
+
arguments = params["arguments"] || {}
|
224
189
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
190
|
+
unless @tools.key?(tool_name)
|
191
|
+
return {
|
192
|
+
jsonrpc: "2.0",
|
193
|
+
id: id,
|
194
|
+
error: {
|
195
|
+
code: -32602,
|
196
|
+
message: "Unknown tool",
|
197
|
+
data: "Tool '#{tool_name}' not found"
|
198
|
+
}
|
199
|
+
}
|
200
|
+
end
|
236
201
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
202
|
+
begin
|
203
|
+
result = call_tool(tool_name, arguments)
|
204
|
+
{
|
205
|
+
jsonrpc: "2.0",
|
206
|
+
id: id,
|
207
|
+
result: {
|
208
|
+
content: [
|
209
|
+
{
|
210
|
+
type: "text",
|
211
|
+
text: result
|
212
|
+
}
|
213
|
+
],
|
214
|
+
isError: false
|
215
|
+
}
|
216
|
+
}
|
217
|
+
rescue => e
|
218
|
+
File.write(Mcpeasy::Config.log_file_path("slack", "error"), "#{Time.now}: Tool error: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
|
219
|
+
{
|
220
|
+
jsonrpc: "2.0",
|
221
|
+
id: id,
|
222
|
+
result: {
|
223
|
+
content: [
|
224
|
+
{
|
225
|
+
type: "text",
|
226
|
+
text: "❌ Error: #{e.message}"
|
227
|
+
}
|
228
|
+
],
|
229
|
+
isError: true
|
230
|
+
}
|
231
|
+
}
|
232
|
+
end
|
243
233
|
end
|
244
|
-
end
|
245
234
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
output << channels.map { |c| "##{c[:name]} (ID: #{c[:id]})" }.join(", ")
|
250
|
-
output
|
251
|
-
end
|
235
|
+
def call_tool(tool_name, arguments)
|
236
|
+
# Initialize Service only when needed
|
237
|
+
@slack_tool ||= Service.new
|
252
238
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
239
|
+
case tool_name
|
240
|
+
when "test_connection"
|
241
|
+
test_connection
|
242
|
+
when "list_channels"
|
243
|
+
list_channels(arguments)
|
244
|
+
when "post_message"
|
245
|
+
post_message(arguments)
|
246
|
+
else
|
247
|
+
raise "Unknown tool: #{tool_name}"
|
248
|
+
end
|
257
249
|
end
|
258
|
-
|
259
|
-
|
250
|
+
|
251
|
+
def test_connection
|
252
|
+
response = @slack_tool.test_connection
|
253
|
+
if response["ok"]
|
254
|
+
"✅ Successfully connected to Slack. Bot: #{response["user"]}, Team: #{response["team"]}"
|
255
|
+
else
|
256
|
+
raise "Authentication failed: #{response["error"]}"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def list_channels(arguments)
|
261
|
+
limit = [arguments["limit"]&.to_i || 100, 1000].min
|
262
|
+
cursor = arguments["cursor"]&.to_s
|
263
|
+
exclude_archived = arguments.key?("exclude_archived") ? arguments["exclude_archived"] : true
|
264
|
+
|
265
|
+
# Keep track of current page for display
|
266
|
+
@list_channels_page ||= {}
|
267
|
+
page_key = cursor || "first"
|
268
|
+
@list_channels_page[page_key] ||= 0
|
269
|
+
@list_channels_page[page_key] = cursor ? @list_channels_page[page_key] + 1 : 1
|
270
|
+
|
271
|
+
result = @slack_tool.list_channels(limit: limit, cursor: cursor&.empty? ? nil : cursor, exclude_archived: exclude_archived)
|
272
|
+
channels = result[:channels]
|
273
|
+
|
274
|
+
# Calculate record range
|
275
|
+
page_num = @list_channels_page[page_key]
|
276
|
+
start_index = (page_num - 1) * limit
|
277
|
+
end_index = start_index + channels.count - 1
|
278
|
+
|
279
|
+
channels_list = channels.map.with_index do |channel, i|
|
280
|
+
"##{channel[:name]} (ID: #{channel[:id]})"
|
281
|
+
end.join(", ")
|
282
|
+
|
283
|
+
pagination_info = if result[:has_more]
|
284
|
+
<<~INFO
|
285
|
+
|
286
|
+
📄 **Page #{page_num}** | Showing channels #{start_index + 1}-#{end_index + 1}
|
287
|
+
_More channels available. Use `cursor: "#{result[:next_cursor]}"` to get the next page._
|
288
|
+
INFO
|
289
|
+
else
|
290
|
+
# Try to estimate total if we're on last page
|
291
|
+
estimated_total = start_index + channels.count
|
292
|
+
<<~INFO
|
293
|
+
|
294
|
+
📄 **Page #{page_num}** | Showing channels #{start_index + 1}-#{end_index + 1} of #{estimated_total} total
|
295
|
+
INFO
|
296
|
+
end
|
297
|
+
|
298
|
+
<<~OUTPUT
|
299
|
+
📋 #{channels.count} Available channels: #{channels_list}#{pagination_info}
|
300
|
+
OUTPUT
|
260
301
|
end
|
261
302
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
303
|
+
def post_message(arguments)
|
304
|
+
# Validate required arguments
|
305
|
+
unless arguments["channel"]
|
306
|
+
raise "Missing required argument: channel"
|
307
|
+
end
|
308
|
+
unless arguments["text"]
|
309
|
+
raise "Missing required argument: text"
|
310
|
+
end
|
266
311
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
thread_ts: thread_ts&.empty? ? nil : thread_ts
|
272
|
-
)
|
312
|
+
channel = arguments["channel"].to_s.sub(/^#/, "")
|
313
|
+
text = arguments["text"].to_s
|
314
|
+
username = arguments["username"]&.to_s
|
315
|
+
thread_ts = arguments["thread_ts"]&.to_s
|
273
316
|
|
274
|
-
|
317
|
+
response = @slack_tool.post_message(
|
318
|
+
channel: channel,
|
319
|
+
text: text,
|
320
|
+
username: username&.empty? ? nil : username,
|
321
|
+
thread_ts: thread_ts&.empty? ? nil : thread_ts
|
322
|
+
)
|
323
|
+
|
324
|
+
"✅ Message posted successfully to ##{channel} (Message timestamp: #{response["ts"]})"
|
325
|
+
end
|
275
326
|
end
|
276
327
|
end
|
277
328
|
|
278
329
|
if __FILE__ == $0
|
279
|
-
MCPServer.new.run
|
330
|
+
Slack::MCPServer.new.run
|
280
331
|
end
|