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.
- checksums.yaml +7 -0
- data/.claude/settings.json +15 -0
- data/.claudeignore +4 -0
- data/.envrc +2 -0
- data/.mcp.json +40 -0
- data/CLAUDE.md +170 -0
- data/README.md +161 -0
- data/bin/mcpz +6 -0
- data/env.template +11 -0
- data/ext/setup.rb +7 -0
- data/lib/mcpeasy/cli.rb +154 -0
- data/lib/mcpeasy/config.rb +102 -0
- data/lib/mcpeasy/setup.rb +22 -0
- data/lib/mcpeasy/version.rb +5 -0
- data/lib/mcpeasy.rb +9 -0
- data/lib/utilities/_google/auth_server.rb +149 -0
- data/lib/utilities/gcal/README.md +237 -0
- data/lib/utilities/gcal/cli.rb +134 -0
- data/lib/utilities/gcal/gcal_tool.rb +308 -0
- data/lib/utilities/gcal/mcp.rb +381 -0
- data/lib/utilities/gdrive/README.md +269 -0
- data/lib/utilities/gdrive/cli.rb +118 -0
- data/lib/utilities/gdrive/gdrive_tool.rb +291 -0
- data/lib/utilities/gdrive/mcp.rb +347 -0
- data/lib/utilities/gmeet/README.md +133 -0
- data/lib/utilities/gmeet/cli.rb +157 -0
- data/lib/utilities/gmeet/gmeet_tool.rb +407 -0
- data/lib/utilities/gmeet/mcp.rb +438 -0
- data/lib/utilities/slack/README.md +211 -0
- data/lib/utilities/slack/cli.rb +74 -0
- data/lib/utilities/slack/mcp.rb +280 -0
- data/lib/utilities/slack/slack_tool.rb +119 -0
- data/logs/.keep +0 -0
- data/mcpeasy.gemspec +47 -0
- metadata +191 -0
@@ -0,0 +1,438 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "json"
|
6
|
+
require_relative "gmeet_tool"
|
7
|
+
|
8
|
+
class MCPServer
|
9
|
+
def initialize
|
10
|
+
# Defer GmeetTool initialization until actually needed
|
11
|
+
@gmeet_tool = nil
|
12
|
+
@tools = {
|
13
|
+
"test_connection" => {
|
14
|
+
name: "test_connection",
|
15
|
+
description: "Test the Google Calendar API connection",
|
16
|
+
inputSchema: {
|
17
|
+
type: "object",
|
18
|
+
properties: {},
|
19
|
+
required: []
|
20
|
+
}
|
21
|
+
},
|
22
|
+
"list_meetings" => {
|
23
|
+
name: "list_meetings",
|
24
|
+
description: "List Google Meet meetings with optional date filtering",
|
25
|
+
inputSchema: {
|
26
|
+
type: "object",
|
27
|
+
properties: {
|
28
|
+
start_date: {
|
29
|
+
type: "string",
|
30
|
+
description: "Start date in YYYY-MM-DD format (default: today)"
|
31
|
+
},
|
32
|
+
end_date: {
|
33
|
+
type: "string",
|
34
|
+
description: "End date in YYYY-MM-DD format (default: 7 days from start)"
|
35
|
+
},
|
36
|
+
max_results: {
|
37
|
+
type: "number",
|
38
|
+
description: "Maximum number of meetings to return (default: 20)"
|
39
|
+
},
|
40
|
+
calendar_id: {
|
41
|
+
type: "string",
|
42
|
+
description: "Calendar ID to list meetings from (default: primary calendar)"
|
43
|
+
}
|
44
|
+
},
|
45
|
+
required: []
|
46
|
+
}
|
47
|
+
},
|
48
|
+
"upcoming_meetings" => {
|
49
|
+
name: "upcoming_meetings",
|
50
|
+
description: "List upcoming Google Meet meetings in the next 24 hours",
|
51
|
+
inputSchema: {
|
52
|
+
type: "object",
|
53
|
+
properties: {
|
54
|
+
max_results: {
|
55
|
+
type: "number",
|
56
|
+
description: "Maximum number of meetings to return (default: 10)"
|
57
|
+
},
|
58
|
+
calendar_id: {
|
59
|
+
type: "string",
|
60
|
+
description: "Calendar ID to list meetings from (default: primary calendar)"
|
61
|
+
}
|
62
|
+
},
|
63
|
+
required: []
|
64
|
+
}
|
65
|
+
},
|
66
|
+
"search_meetings" => {
|
67
|
+
name: "search_meetings",
|
68
|
+
description: "Search for Google Meet meetings by text content",
|
69
|
+
inputSchema: {
|
70
|
+
type: "object",
|
71
|
+
properties: {
|
72
|
+
query: {
|
73
|
+
type: "string",
|
74
|
+
description: "Search query to find meetings"
|
75
|
+
},
|
76
|
+
start_date: {
|
77
|
+
type: "string",
|
78
|
+
description: "Start date in YYYY-MM-DD format (default: today)"
|
79
|
+
},
|
80
|
+
end_date: {
|
81
|
+
type: "string",
|
82
|
+
description: "End date in YYYY-MM-DD format (default: 30 days from start)"
|
83
|
+
},
|
84
|
+
max_results: {
|
85
|
+
type: "number",
|
86
|
+
description: "Maximum number of meetings to return (default: 10)"
|
87
|
+
}
|
88
|
+
},
|
89
|
+
required: ["query"]
|
90
|
+
}
|
91
|
+
},
|
92
|
+
"get_meeting_url" => {
|
93
|
+
name: "get_meeting_url",
|
94
|
+
description: "Get the Google Meet URL for a specific event",
|
95
|
+
inputSchema: {
|
96
|
+
type: "object",
|
97
|
+
properties: {
|
98
|
+
event_id: {
|
99
|
+
type: "string",
|
100
|
+
description: "Calendar event ID"
|
101
|
+
},
|
102
|
+
calendar_id: {
|
103
|
+
type: "string",
|
104
|
+
description: "Calendar ID (default: primary calendar)"
|
105
|
+
}
|
106
|
+
},
|
107
|
+
required: ["event_id"]
|
108
|
+
}
|
109
|
+
}
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
def run
|
114
|
+
# Disable stdout buffering for immediate response
|
115
|
+
$stdout.sync = true
|
116
|
+
|
117
|
+
# Log startup to file instead of stdout to avoid protocol interference
|
118
|
+
Mcpeasy::Config.ensure_config_dirs
|
119
|
+
File.write(Mcpeasy::Config.log_file_path("gmeet", "startup"), "#{Time.now}: Google Meet MCP Server starting on stdio\n", mode: "a")
|
120
|
+
while (line = $stdin.gets)
|
121
|
+
handle_request(line.strip)
|
122
|
+
end
|
123
|
+
rescue Interrupt
|
124
|
+
# Silent shutdown
|
125
|
+
rescue => e
|
126
|
+
# Log to a file instead of stderr to avoid protocol interference
|
127
|
+
File.write(Mcpeasy::Config.log_file_path("gmeet", "error"), "#{Time.now}: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def handle_request(line)
|
133
|
+
return if line.empty?
|
134
|
+
|
135
|
+
begin
|
136
|
+
request = JSON.parse(line)
|
137
|
+
response = process_request(request)
|
138
|
+
if response
|
139
|
+
puts JSON.generate(response)
|
140
|
+
$stdout.flush
|
141
|
+
end
|
142
|
+
rescue JSON::ParserError => e
|
143
|
+
error_response = {
|
144
|
+
jsonrpc: "2.0",
|
145
|
+
id: nil,
|
146
|
+
error: {
|
147
|
+
code: -32700,
|
148
|
+
message: "Parse error",
|
149
|
+
data: e.message
|
150
|
+
}
|
151
|
+
}
|
152
|
+
puts JSON.generate(error_response)
|
153
|
+
$stdout.flush
|
154
|
+
rescue => e
|
155
|
+
File.write(Mcpeasy::Config.log_file_path("gmeet", "error"), "#{Time.now}: Error handling request: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
|
156
|
+
error_response = {
|
157
|
+
jsonrpc: "2.0",
|
158
|
+
id: request&.dig("id"),
|
159
|
+
error: {
|
160
|
+
code: -32603,
|
161
|
+
message: "Internal error",
|
162
|
+
data: e.message
|
163
|
+
}
|
164
|
+
}
|
165
|
+
puts JSON.generate(error_response)
|
166
|
+
$stdout.flush
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def process_request(request)
|
171
|
+
id = request["id"]
|
172
|
+
method = request["method"]
|
173
|
+
params = request["params"] || {}
|
174
|
+
|
175
|
+
case method
|
176
|
+
when "notifications/initialized"
|
177
|
+
# Client acknowledgment - no response needed
|
178
|
+
nil
|
179
|
+
when "initialize"
|
180
|
+
initialize_response(id, params)
|
181
|
+
when "tools/list"
|
182
|
+
tools_list_response(id, params)
|
183
|
+
when "tools/call"
|
184
|
+
tools_call_response(id, params)
|
185
|
+
else
|
186
|
+
{
|
187
|
+
jsonrpc: "2.0",
|
188
|
+
id: id,
|
189
|
+
error: {
|
190
|
+
code: -32601,
|
191
|
+
message: "Method not found",
|
192
|
+
data: "Unknown method: #{method}"
|
193
|
+
}
|
194
|
+
}
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def initialize_response(id, params)
|
199
|
+
{
|
200
|
+
jsonrpc: "2.0",
|
201
|
+
id: id,
|
202
|
+
result: {
|
203
|
+
protocolVersion: "2024-11-05",
|
204
|
+
capabilities: {
|
205
|
+
tools: {}
|
206
|
+
},
|
207
|
+
serverInfo: {
|
208
|
+
name: "gmeet-mcp-server",
|
209
|
+
version: "1.0.0"
|
210
|
+
}
|
211
|
+
}
|
212
|
+
}
|
213
|
+
end
|
214
|
+
|
215
|
+
def tools_list_response(id, params)
|
216
|
+
{
|
217
|
+
jsonrpc: "2.0",
|
218
|
+
id: id,
|
219
|
+
result: {
|
220
|
+
tools: @tools.values
|
221
|
+
}
|
222
|
+
}
|
223
|
+
end
|
224
|
+
|
225
|
+
def tools_call_response(id, params)
|
226
|
+
tool_name = params["name"]
|
227
|
+
arguments = params["arguments"] || {}
|
228
|
+
|
229
|
+
unless @tools.key?(tool_name)
|
230
|
+
return {
|
231
|
+
jsonrpc: "2.0",
|
232
|
+
id: id,
|
233
|
+
error: {
|
234
|
+
code: -32602,
|
235
|
+
message: "Unknown tool",
|
236
|
+
data: "Tool '#{tool_name}' not found"
|
237
|
+
}
|
238
|
+
}
|
239
|
+
end
|
240
|
+
|
241
|
+
begin
|
242
|
+
result = call_tool(tool_name, arguments)
|
243
|
+
{
|
244
|
+
jsonrpc: "2.0",
|
245
|
+
id: id,
|
246
|
+
result: {
|
247
|
+
content: [
|
248
|
+
{
|
249
|
+
type: "text",
|
250
|
+
text: result
|
251
|
+
}
|
252
|
+
],
|
253
|
+
isError: false
|
254
|
+
}
|
255
|
+
}
|
256
|
+
rescue => e
|
257
|
+
File.write(Mcpeasy::Config.log_file_path("gmeet", "error"), "#{Time.now}: Tool error: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
|
258
|
+
{
|
259
|
+
jsonrpc: "2.0",
|
260
|
+
id: id,
|
261
|
+
result: {
|
262
|
+
content: [
|
263
|
+
{
|
264
|
+
type: "text",
|
265
|
+
text: "❌ Error: #{e.message}"
|
266
|
+
}
|
267
|
+
],
|
268
|
+
isError: true
|
269
|
+
}
|
270
|
+
}
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def call_tool(tool_name, arguments)
|
275
|
+
# Initialize GmeetTool only when needed
|
276
|
+
@gmeet_tool ||= GmeetTool.new
|
277
|
+
|
278
|
+
case tool_name
|
279
|
+
when "test_connection"
|
280
|
+
test_connection
|
281
|
+
when "list_meetings"
|
282
|
+
list_meetings(arguments)
|
283
|
+
when "upcoming_meetings"
|
284
|
+
upcoming_meetings(arguments)
|
285
|
+
when "search_meetings"
|
286
|
+
search_meetings(arguments)
|
287
|
+
when "get_meeting_url"
|
288
|
+
get_meeting_url(arguments)
|
289
|
+
else
|
290
|
+
raise "Unknown tool: #{tool_name}"
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def test_connection
|
295
|
+
response = @gmeet_tool.test_connection
|
296
|
+
if response[:ok]
|
297
|
+
"✅ Successfully connected to Google Calendar.\n" \
|
298
|
+
" User: #{response[:user]} (#{response[:email]})"
|
299
|
+
else
|
300
|
+
raise "Connection test failed"
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def list_meetings(arguments)
|
305
|
+
start_date = arguments["start_date"]
|
306
|
+
end_date = arguments["end_date"]
|
307
|
+
max_results = arguments["max_results"]&.to_i || 20
|
308
|
+
calendar_id = arguments["calendar_id"] || "primary"
|
309
|
+
|
310
|
+
result = @gmeet_tool.list_meetings(
|
311
|
+
start_date: start_date,
|
312
|
+
end_date: end_date,
|
313
|
+
max_results: max_results,
|
314
|
+
calendar_id: calendar_id
|
315
|
+
)
|
316
|
+
meetings = result[:meetings]
|
317
|
+
|
318
|
+
if meetings.empty?
|
319
|
+
"🎥 No Google Meet meetings found for the specified date range"
|
320
|
+
else
|
321
|
+
output = "🎥 Found #{result[:count]} Google Meet meeting(s):\n\n"
|
322
|
+
meetings.each_with_index do |meeting, index|
|
323
|
+
output << "#{index + 1}. **#{meeting[:summary] || "No title"}**\n"
|
324
|
+
output << " - Start: #{format_datetime(meeting[:start])}\n"
|
325
|
+
output << " - End: #{format_datetime(meeting[:end])}\n"
|
326
|
+
output << " - Description: #{meeting[:description] || "No description"}\n" if meeting[:description]
|
327
|
+
output << " - Location: #{meeting[:location]}\n" if meeting[:location]
|
328
|
+
output << " - Attendees: #{meeting[:attendees].join(", ")}\n" if meeting[:attendees]&.any?
|
329
|
+
output << " - **Meet Link: #{meeting[:meet_link]}**\n"
|
330
|
+
output << " - Calendar Link: #{meeting[:html_link]}\n\n"
|
331
|
+
end
|
332
|
+
output
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def upcoming_meetings(arguments)
|
337
|
+
max_results = arguments["max_results"]&.to_i || 10
|
338
|
+
calendar_id = arguments["calendar_id"] || "primary"
|
339
|
+
|
340
|
+
result = @gmeet_tool.upcoming_meetings(
|
341
|
+
max_results: max_results,
|
342
|
+
calendar_id: calendar_id
|
343
|
+
)
|
344
|
+
meetings = result[:meetings]
|
345
|
+
|
346
|
+
if meetings.empty?
|
347
|
+
"🎥 No upcoming Google Meet meetings found in the next 24 hours"
|
348
|
+
else
|
349
|
+
output = "🎥 Found #{result[:count]} upcoming Google Meet meeting(s):\n\n"
|
350
|
+
meetings.each_with_index do |meeting, index|
|
351
|
+
output << "#{index + 1}. **#{meeting[:summary] || "No title"}**\n"
|
352
|
+
output << " - Start: #{format_datetime(meeting[:start])} (#{meeting[:time_until_start]})\n"
|
353
|
+
output << " - End: #{format_datetime(meeting[:end])}\n"
|
354
|
+
output << " - Description: #{meeting[:description] || "No description"}\n" if meeting[:description]
|
355
|
+
output << " - Location: #{meeting[:location]}\n" if meeting[:location]
|
356
|
+
output << " - Attendees: #{meeting[:attendees].join(", ")}\n" if meeting[:attendees]&.any?
|
357
|
+
output << " - **Meet Link: #{meeting[:meet_link]}**\n"
|
358
|
+
output << " - Calendar Link: #{meeting[:html_link]}\n\n"
|
359
|
+
end
|
360
|
+
output
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
def search_meetings(arguments)
|
365
|
+
unless arguments["query"]
|
366
|
+
raise "Missing required argument: query"
|
367
|
+
end
|
368
|
+
|
369
|
+
query = arguments["query"].to_s
|
370
|
+
start_date = arguments["start_date"]
|
371
|
+
end_date = arguments["end_date"]
|
372
|
+
max_results = arguments["max_results"]&.to_i || 10
|
373
|
+
|
374
|
+
result = @gmeet_tool.search_meetings(
|
375
|
+
query,
|
376
|
+
start_date: start_date,
|
377
|
+
end_date: end_date,
|
378
|
+
max_results: max_results
|
379
|
+
)
|
380
|
+
meetings = result[:meetings]
|
381
|
+
|
382
|
+
if meetings.empty?
|
383
|
+
"🔍 No Google Meet meetings found matching '#{query}'"
|
384
|
+
else
|
385
|
+
output = "🔍 Found #{result[:count]} Google Meet meeting(s) matching '#{query}':\n\n"
|
386
|
+
meetings.each_with_index do |meeting, index|
|
387
|
+
output << "#{index + 1}. **#{meeting[:summary] || "No title"}**\n"
|
388
|
+
output << " - Start: #{format_datetime(meeting[:start])}\n"
|
389
|
+
output << " - End: #{format_datetime(meeting[:end])}\n"
|
390
|
+
output << " - Description: #{meeting[:description] || "No description"}\n" if meeting[:description]
|
391
|
+
output << " - Location: #{meeting[:location]}\n" if meeting[:location]
|
392
|
+
output << " - **Meet Link: #{meeting[:meet_link]}**\n"
|
393
|
+
output << " - Calendar Link: #{meeting[:html_link]}\n\n"
|
394
|
+
end
|
395
|
+
output
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
def get_meeting_url(arguments)
|
400
|
+
unless arguments["event_id"]
|
401
|
+
raise "Missing required argument: event_id"
|
402
|
+
end
|
403
|
+
|
404
|
+
event_id = arguments["event_id"].to_s
|
405
|
+
calendar_id = arguments["calendar_id"] || "primary"
|
406
|
+
|
407
|
+
result = @gmeet_tool.get_meeting_url(event_id, calendar_id: calendar_id)
|
408
|
+
|
409
|
+
output = "🎥 **#{result[:summary] || "Meeting"}**\n"
|
410
|
+
output << " - Start: #{format_datetime(result[:start])}\n"
|
411
|
+
output << " - End: #{format_datetime(result[:end])}\n"
|
412
|
+
output << " - **Meet Link: #{result[:meet_link]}**\n"
|
413
|
+
output << " - Event ID: #{result[:event_id]}\n"
|
414
|
+
|
415
|
+
output
|
416
|
+
end
|
417
|
+
|
418
|
+
private
|
419
|
+
|
420
|
+
def format_datetime(datetime_info)
|
421
|
+
return "Unknown" unless datetime_info
|
422
|
+
|
423
|
+
if datetime_info[:date]
|
424
|
+
# All-day event
|
425
|
+
datetime_info[:date]
|
426
|
+
elsif datetime_info[:date_time]
|
427
|
+
# Specific time event
|
428
|
+
time = Time.parse(datetime_info[:date_time])
|
429
|
+
time.strftime("%Y-%m-%d %H:%M")
|
430
|
+
else
|
431
|
+
"Unknown"
|
432
|
+
end
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
if __FILE__ == $0
|
437
|
+
MCPServer.new.run
|
438
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
# Slack MCP Server
|
2
|
+
|
3
|
+
A Ruby-based Model Context Protocol (MCP) server that provides programmatic access to Slack's Web API. This server can operate in two modes:
|
4
|
+
|
5
|
+
1. **CLI script**: Direct command-line usage for posting messages to channels
|
6
|
+
2. **MCP server**: Integration with AI assistants like Claude Code
|
7
|
+
|
8
|
+
## Features
|
9
|
+
|
10
|
+
- 💬 **Post messages** to Slack channels with optional custom usernames
|
11
|
+
- 📋 **List channels** to see available public and private channels
|
12
|
+
- 🔐 **Bot token authentication** with secure credential storage
|
13
|
+
- 🛡️ **Error handling** with comprehensive API failure reporting
|
14
|
+
- ✅ **Connection testing** to verify authentication before operations
|
15
|
+
|
16
|
+
## Prerequisites
|
17
|
+
|
18
|
+
### 1. Ruby Environment
|
19
|
+
|
20
|
+
```bash
|
21
|
+
# Install Ruby dependencies
|
22
|
+
bundle install
|
23
|
+
```
|
24
|
+
|
25
|
+
Required gems:
|
26
|
+
- `slack-ruby-client` - Slack Web API client
|
27
|
+
- `standard` - Ruby code linting
|
28
|
+
|
29
|
+
### 2. Slack App Setup
|
30
|
+
|
31
|
+
#### Step 1: Create a Slack App
|
32
|
+
|
33
|
+
1. Go to https://api.slack.com/apps
|
34
|
+
2. Click **"Create New App"** → **"From scratch"**
|
35
|
+
3. Give your app a name and select your workspace
|
36
|
+
4. Note your app's details
|
37
|
+
|
38
|
+
#### Step 2: Configure OAuth Permissions
|
39
|
+
|
40
|
+
1. Go to **"OAuth & Permissions"** in the sidebar
|
41
|
+
2. Under **"Scopes"** → **"Bot Token Scopes"**, add these permissions:
|
42
|
+
- `chat:write` - Required to post messages
|
43
|
+
- `channels:read` - Optional, for listing public channels
|
44
|
+
- `groups:read` - Optional, for listing private channels
|
45
|
+
3. Click **"Install to Workspace"** at the top
|
46
|
+
4. Copy the **"Bot User OAuth Token"** (starts with `xoxb-`)
|
47
|
+
|
48
|
+
#### Step 3: Configure Credentials
|
49
|
+
|
50
|
+
Configure your Slack bot token using the gem's configuration system:
|
51
|
+
|
52
|
+
```bash
|
53
|
+
mcpz slack set_bot_token xoxb-your-actual-slack-token
|
54
|
+
```
|
55
|
+
|
56
|
+
This will store your bot token securely in `~/.config/mcpeasy/slack.json`.
|
57
|
+
|
58
|
+
## Usage
|
59
|
+
|
60
|
+
### CLI Mode
|
61
|
+
|
62
|
+
#### Test Connection
|
63
|
+
```bash
|
64
|
+
mcpz slack test
|
65
|
+
```
|
66
|
+
|
67
|
+
#### Post Messages
|
68
|
+
```bash
|
69
|
+
# Post a simple message
|
70
|
+
mcpz slack post general "Hello from Ruby!"
|
71
|
+
|
72
|
+
# Post with a custom username
|
73
|
+
mcpz slack post general "Deployment completed successfully!" --username "DeployBot"
|
74
|
+
|
75
|
+
# Use channel with or without # prefix
|
76
|
+
mcpz slack post "#general" "Message with # prefix"
|
77
|
+
```
|
78
|
+
|
79
|
+
#### List Channels
|
80
|
+
```bash
|
81
|
+
# List all available channels
|
82
|
+
mcpz slack channels
|
83
|
+
```
|
84
|
+
|
85
|
+
### MCP Server Mode
|
86
|
+
|
87
|
+
#### Configuration for Claude Code
|
88
|
+
|
89
|
+
Add to your `.mcp.json` configuration:
|
90
|
+
|
91
|
+
```json
|
92
|
+
{
|
93
|
+
"mcpServers": {
|
94
|
+
"slack": {
|
95
|
+
"command": "mcpz",
|
96
|
+
"args": ["slack", "mcp"]
|
97
|
+
}
|
98
|
+
}
|
99
|
+
}
|
100
|
+
```
|
101
|
+
|
102
|
+
#### Run as Standalone MCP Server
|
103
|
+
|
104
|
+
```bash
|
105
|
+
mcpz slack mcp
|
106
|
+
```
|
107
|
+
|
108
|
+
The server provides these tools to Claude Code:
|
109
|
+
|
110
|
+
- **test_connection**: Test Slack API connectivity
|
111
|
+
- **post_message**: Post messages to channels with optional custom username
|
112
|
+
- **list_channels**: List available public and private channels
|
113
|
+
|
114
|
+
## Security & Permissions
|
115
|
+
|
116
|
+
### Required OAuth Scopes
|
117
|
+
|
118
|
+
- `chat:write` - Required to post messages
|
119
|
+
- `channels:read` - Optional, for listing public channels
|
120
|
+
- `groups:read` - Optional, for listing private channels
|
121
|
+
|
122
|
+
### Local File Storage
|
123
|
+
|
124
|
+
- **Credentials**: Stored in `~/.config/mcpeasy/slack.json`
|
125
|
+
- **Logs**: Application logs for debugging
|
126
|
+
|
127
|
+
### Best Practices
|
128
|
+
|
129
|
+
1. **Never commit** bot tokens to version control
|
130
|
+
2. **Limit permissions** to only what's needed for your use case
|
131
|
+
3. **Regular rotation** of bot tokens (recommended annually)
|
132
|
+
4. **Monitor usage** through Slack's app management dashboard
|
133
|
+
|
134
|
+
## Troubleshooting
|
135
|
+
|
136
|
+
### Common Issues
|
137
|
+
|
138
|
+
#### "Invalid auth" Error
|
139
|
+
- Check that your bot token is correct and starts with `xoxb-`
|
140
|
+
- Re-run: `mcpz slack set_bot_token xoxb-your-actual-slack-token`
|
141
|
+
- Verify the token hasn't expired or been revoked
|
142
|
+
- Ensure the app is installed in your workspace
|
143
|
+
|
144
|
+
#### "Missing scope" Error
|
145
|
+
- Add the required OAuth scopes in your Slack app configuration
|
146
|
+
- Reinstall the app to workspace after adding scopes
|
147
|
+
- Required scopes: `chat:write` (minimum), `channels:read`, `groups:read` (optional)
|
148
|
+
|
149
|
+
#### "Channel not found" Error
|
150
|
+
- Verify the channel name is spelled correctly
|
151
|
+
- Ensure your bot has access to the channel (invite the bot if needed)
|
152
|
+
- Try using the channel ID instead of the name
|
153
|
+
|
154
|
+
#### "Bot not in channel" Error
|
155
|
+
- Invite your bot to the channel: `/invite @your-bot-name`
|
156
|
+
- Or use channels where the bot is already a member
|
157
|
+
|
158
|
+
### Testing the Setup
|
159
|
+
|
160
|
+
1. **Test CLI authentication**:
|
161
|
+
```bash
|
162
|
+
mcpz slack test
|
163
|
+
```
|
164
|
+
|
165
|
+
2. **Test MCP server**:
|
166
|
+
```bash
|
167
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | mcpz slack mcp
|
168
|
+
```
|
169
|
+
|
170
|
+
## Development
|
171
|
+
|
172
|
+
### File Structure
|
173
|
+
|
174
|
+
```
|
175
|
+
lib/utilities/slack/
|
176
|
+
├── cli.rb # Thor-based CLI interface
|
177
|
+
├── mcp.rb # MCP server implementation
|
178
|
+
├── slack_tool.rb # Slack Web API wrapper
|
179
|
+
└── README.md # This file
|
180
|
+
```
|
181
|
+
|
182
|
+
### Adding New Features
|
183
|
+
|
184
|
+
1. **New API methods**: Add to `SlackTool` class
|
185
|
+
2. **New CLI commands**: Add to `SlackCLI` class
|
186
|
+
3. **New MCP tools**: Add to `MCPServer` class
|
187
|
+
|
188
|
+
### Testing
|
189
|
+
|
190
|
+
```bash
|
191
|
+
# Run Ruby linting
|
192
|
+
bundle exec standardrb
|
193
|
+
|
194
|
+
# Test CLI commands
|
195
|
+
mcpz slack test
|
196
|
+
mcpz slack channels
|
197
|
+
|
198
|
+
# Test MCP server manually
|
199
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | mcpz slack mcp
|
200
|
+
```
|
201
|
+
|
202
|
+
## Contributing
|
203
|
+
|
204
|
+
1. Follow existing code patterns and style
|
205
|
+
2. Add comprehensive error handling
|
206
|
+
3. Update this README for new features
|
207
|
+
4. Test both CLI and MCP modes
|
208
|
+
|
209
|
+
## License
|
210
|
+
|
211
|
+
This project follows the same license as the parent repository.
|