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,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "thor"
5
+ require_relative "slack_tool"
6
+
7
+ class SlackCLI < Thor
8
+ desc "test", "Test the Slack API connection"
9
+ def test
10
+ response = tool.test_connection
11
+
12
+ if response["ok"]
13
+ puts "✅ Successfully connected to Slack"
14
+ puts " Bot name: #{response["user"]}"
15
+ puts " Team: #{response["team"]}"
16
+ else
17
+ warn "❌ Authentication failed: #{response["error"]}"
18
+ end
19
+ rescue RuntimeError => e
20
+ puts "❌ Failed to connect to Slack: #{e.message}"
21
+ exit 1
22
+ end
23
+
24
+ desc "list", "List available Slack channels"
25
+ def list
26
+ channels = tool.list_channels
27
+
28
+ if channels && !channels.empty?
29
+ puts "📋 Available channels:"
30
+ channels.each do |channel|
31
+ puts " ##{channel[:name]} (ID: #{channel[:id]})"
32
+ end
33
+ end
34
+ rescue RuntimeError => e
35
+ warn "❌ Failed to list channels: #{e.message}"
36
+ exit 1
37
+ end
38
+
39
+ desc "post", "Post a message to a Slack channel"
40
+ method_option :channel, required: true, type: :string, aliases: "-c"
41
+ method_option :message, required: true, type: :string, aliases: "-m"
42
+ method_option :username, type: :string, aliases: "-u"
43
+ method_option :timestamp, type: :string, aliases: "-t"
44
+ def post
45
+ channel = options[:channel]
46
+ text = options[:message]
47
+ username = options[:username]
48
+ thread_ts = options[:timestamp]
49
+
50
+ response = tool.post_message(
51
+ channel: channel,
52
+ text: text,
53
+ username: username,
54
+ thread_ts: thread_ts
55
+ )
56
+
57
+ if response["ok"]
58
+ puts "✅ Message posted successfully to ##{channel}"
59
+ puts " Message timestamp: #{response["ts"]}"
60
+ else
61
+ warn "❌ Failed to post message: #{response["error"]}"
62
+ exit 1
63
+ end
64
+ rescue RuntimeError => e
65
+ warn "❌ Unexpected error: #{e.message}"
66
+ exit 1
67
+ end
68
+
69
+ private
70
+
71
+ def tool
72
+ @tool ||= SlackTool.new
73
+ end
74
+ end
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "json"
6
+ require_relative "slack_tool"
7
+
8
+ class MCPServer
9
+ def initialize
10
+ # Defer SlackTool initialization until actually needed
11
+ @slack_tool = nil
12
+ @tools = {
13
+ "test_connection" => {
14
+ name: "test_connection",
15
+ description: "Test the Slack API connection",
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {},
19
+ required: []
20
+ }
21
+ },
22
+ "list_channels" => {
23
+ name: "list_channels",
24
+ description: "List available Slack channels",
25
+ inputSchema: {
26
+ type: "object",
27
+ properties: {},
28
+ required: []
29
+ }
30
+ },
31
+ "post_message" => {
32
+ name: "post_message",
33
+ description: "Post a message to a Slack channel",
34
+ inputSchema: {
35
+ type: "object",
36
+ properties: {
37
+ channel: {
38
+ type: "string",
39
+ description: "The Slack channel name (with or without #)"
40
+ },
41
+ text: {
42
+ type: "string",
43
+ description: "The message text to post"
44
+ },
45
+ username: {
46
+ type: "string",
47
+ description: "Optional custom username for the message"
48
+ },
49
+ thread_ts: {
50
+ type: "string",
51
+ description: "Optional timestamp of parent message to reply to"
52
+ }
53
+ },
54
+ required: ["channel", "text"]
55
+ }
56
+ }
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
+ 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
+
77
+ private
78
+
79
+ def handle_request(line)
80
+ return if line.empty?
81
+
82
+ begin
83
+ request = JSON.parse(line)
84
+ response = process_request(request)
85
+ if response
86
+ puts JSON.generate(response)
87
+ $stdout.flush
88
+ end
89
+ rescue JSON::ParserError => e
90
+ error_response = {
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
101
+ rescue => e
102
+ File.write(Mcpeasy::Config.log_file_path("slack", "error"), "#{Time.now}: Error handling request: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
103
+ error_response = {
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
114
+ end
115
+ end
116
+
117
+ def process_request(request)
118
+ id = request["id"]
119
+ method = request["method"]
120
+ params = request["params"] || {}
121
+
122
+ case method
123
+ when "notifications/initialized"
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
144
+
145
+ def initialize_response(id, params)
146
+ {
147
+ jsonrpc: "2.0",
148
+ id: id,
149
+ result: {
150
+ protocolVersion: "2024-11-05",
151
+ capabilities: {
152
+ tools: {}
153
+ },
154
+ serverInfo: {
155
+ name: "slack-mcp-server",
156
+ version: "1.0.0"
157
+ }
158
+ }
159
+ }
160
+ end
161
+
162
+ def tools_list_response(id, params)
163
+ {
164
+ jsonrpc: "2.0",
165
+ id: id,
166
+ result: {
167
+ tools: @tools.values
168
+ }
169
+ }
170
+ end
171
+
172
+ def tools_call_response(id, params)
173
+ tool_name = params["name"]
174
+ arguments = params["arguments"] || {}
175
+
176
+ unless @tools.key?(tool_name)
177
+ return {
178
+ jsonrpc: "2.0",
179
+ id: id,
180
+ error: {
181
+ code: -32602,
182
+ message: "Unknown tool",
183
+ data: "Tool '#{tool_name}' not found"
184
+ }
185
+ }
186
+ end
187
+
188
+ begin
189
+ result = call_tool(tool_name, arguments)
190
+ {
191
+ jsonrpc: "2.0",
192
+ id: id,
193
+ result: {
194
+ content: [
195
+ {
196
+ type: "text",
197
+ text: result
198
+ }
199
+ ],
200
+ isError: false
201
+ }
202
+ }
203
+ rescue => e
204
+ File.write(Mcpeasy::Config.log_file_path("slack", "error"), "#{Time.now}: Tool error: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
205
+ {
206
+ jsonrpc: "2.0",
207
+ id: id,
208
+ result: {
209
+ content: [
210
+ {
211
+ type: "text",
212
+ text: "❌ Error: #{e.message}"
213
+ }
214
+ ],
215
+ isError: true
216
+ }
217
+ }
218
+ end
219
+ end
220
+
221
+ def call_tool(tool_name, arguments)
222
+ # Initialize SlackTool only when needed
223
+ @slack_tool ||= SlackTool.new
224
+
225
+ case tool_name
226
+ when "test_connection"
227
+ test_connection
228
+ when "list_channels"
229
+ list_channels
230
+ when "post_message"
231
+ post_message(arguments)
232
+ else
233
+ raise "Unknown tool: #{tool_name}"
234
+ end
235
+ end
236
+
237
+ def test_connection
238
+ response = @slack_tool.test_connection
239
+ if response["ok"]
240
+ "✅ Successfully connected to Slack. Bot: #{response["user"]}, Team: #{response["team"]}"
241
+ else
242
+ raise "Authentication failed: #{response["error"]}"
243
+ end
244
+ end
245
+
246
+ def list_channels
247
+ channels = @slack_tool.list_channels
248
+ output = "📋 #{channels.count} Available channels: "
249
+ output << channels.map { |c| "##{c[:name]} (ID: #{c[:id]})" }.join(", ")
250
+ output
251
+ end
252
+
253
+ def post_message(arguments)
254
+ # Validate required arguments
255
+ unless arguments["channel"]
256
+ raise "Missing required argument: channel"
257
+ end
258
+ unless arguments["text"]
259
+ raise "Missing required argument: text"
260
+ end
261
+
262
+ channel = arguments["channel"].to_s.sub(/^#/, "")
263
+ text = arguments["text"].to_s
264
+ username = arguments["username"]&.to_s
265
+ thread_ts = arguments["thread_ts"]&.to_s
266
+
267
+ response = @slack_tool.post_message(
268
+ channel: channel,
269
+ text: text,
270
+ username: username&.empty? ? nil : username,
271
+ thread_ts: thread_ts&.empty? ? nil : thread_ts
272
+ )
273
+
274
+ "✅ Message posted successfully to ##{channel} (Message timestamp: #{response["ts"]})"
275
+ end
276
+ end
277
+
278
+ if __FILE__ == $0
279
+ MCPServer.new.run
280
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "slack-ruby-client"
5
+ require "mcpeasy/config"
6
+
7
+ class SlackTool
8
+ def initialize
9
+ ensure_env!
10
+ @client = Slack::Web::Client.new(
11
+ token: Mcpeasy::Config.slack_bot_token,
12
+ timeout: 10, # 10 second timeout
13
+ open_timeout: 5 # 5 second connection timeout
14
+ )
15
+ end
16
+
17
+ def post_message(channel:, text:, username: nil, thread_ts: nil)
18
+ # Clean up parameters
19
+ clean_channel = channel.to_s.sub(/^#/, "").strip
20
+ clean_text = text.to_s.strip
21
+
22
+ # Validate inputs
23
+ raise "Channel cannot be empty" if clean_channel.empty?
24
+ raise "Text cannot be empty" if clean_text.empty?
25
+
26
+ # Build request parameters
27
+ params = {
28
+ channel: clean_channel,
29
+ text: clean_text
30
+ }
31
+ params[:username] = username if username && !username.to_s.strip.empty?
32
+ params[:thread_ts] = thread_ts if thread_ts && !thread_ts.to_s.strip.empty?
33
+
34
+ # Retry logic for reliability
35
+ max_retries = 3
36
+ retry_count = 0
37
+
38
+ begin
39
+ response = @client.chat_postMessage(params)
40
+
41
+ if response["ok"]
42
+ response
43
+ else
44
+ raise "Failed to post message: #{response["error"]} (#{response.inspect})"
45
+ end
46
+ rescue Slack::Web::Api::Errors::TooManyRequestsError => e
47
+ retry_count += 1
48
+ if retry_count <= max_retries
49
+ sleep_time = e.retry_after || 1
50
+ sleep(sleep_time)
51
+ retry
52
+ else
53
+ raise "Slack API Error: #{e.message}"
54
+ end
55
+ rescue Slack::Web::Api::Errors::SlackError => e
56
+ retry_count += 1
57
+ if retry_count <= max_retries && retryable_error?(e)
58
+ sleep(0.5 * retry_count) # Exponential backoff
59
+ retry
60
+ else
61
+ raise "Slack API Error: #{e.message}"
62
+ end
63
+ rescue => e
64
+ Mcpeasy::Config.ensure_config_dirs
65
+ File.write(Mcpeasy::Config.log_file_path("slack", "error"), "#{Time.now}: SlackTool error: #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}\n", mode: "a")
66
+ raise e
67
+ end
68
+ end
69
+
70
+ def list_channels
71
+ response = @client.conversations_list(types: "public_channel,private_channel")
72
+
73
+ if response["ok"]
74
+ response["channels"].to_a.map do |channel|
75
+ {
76
+ name: channel["name"],
77
+ id: channel["id"]
78
+ }
79
+ end
80
+ else
81
+ raise "Failed to list channels: #{response["error"]}"
82
+ end
83
+ rescue Slack::Web::Api::Errors::SlackError => e
84
+ raise "Slack API Error: #{e.message}"
85
+ end
86
+
87
+ def test_connection
88
+ response = @client.auth_test
89
+
90
+ if response["ok"]
91
+ response
92
+ else
93
+ raise "Authentication failed: #{response["error"]}"
94
+ end
95
+ rescue Slack::Web::Api::Errors::SlackError => e
96
+ raise "Slack API Error: #{e.message}"
97
+ end
98
+
99
+ def tool_definitions
100
+ end
101
+
102
+ private
103
+
104
+ def retryable_error?(error)
105
+ # Network-related errors that might be temporary
106
+ error.is_a?(Slack::Web::Api::Errors::TimeoutError) ||
107
+ error.is_a?(Slack::Web::Api::Errors::UnavailableError) ||
108
+ (error.respond_to?(:message) && error.message.include?("timeout"))
109
+ end
110
+
111
+ def ensure_env!
112
+ unless Mcpeasy::Config.slack_bot_token
113
+ raise <<~ERROR
114
+ Slack bot token is not configured!
115
+ Please run: mcp set slack_bot_token YOUR_TOKEN
116
+ ERROR
117
+ end
118
+ end
119
+ end
data/logs/.keep ADDED
File without changes
data/mcpeasy.gemspec ADDED
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/mcpeasy/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "mcpeasy"
7
+ spec.version = Mcpeasy::VERSION
8
+ spec.authors = ["Joel Helbling"]
9
+ spec.email = ["joel@joelhelbling.com"]
10
+
11
+ spec.summary = "MCP servers made easy"
12
+ spec.description = "mcpeasy, LM squeezy - Easy-to-use MCP servers for Google Calendar, Google Drive, Google Meet, and Slack"
13
+ spec.homepage = "https://github.com/joelhelbling/mcpeasy"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.0.0"
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = "https://github.com/joelhelbling/mcpeasy"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (File.expand_path(f) == __FILE__) ||
25
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
26
+ end
27
+ end
28
+
29
+ spec.bindir = "bin"
30
+ spec.executables = ["mcpz"]
31
+ spec.require_paths = ["lib"]
32
+
33
+ # Dependencies
34
+ spec.add_dependency "google-apis-calendar_v3", "~> 0.35"
35
+ spec.add_dependency "google-apis-drive_v3", "~> 0.45"
36
+ spec.add_dependency "googleauth", "~> 1.8"
37
+ spec.add_dependency "slack-ruby-client", "~> 2.1"
38
+ spec.add_dependency "webrick", "~> 1.8"
39
+ spec.add_dependency "thor", "~> 1.3"
40
+ spec.add_dependency "mother", "~> 0.1"
41
+
42
+ # Development dependencies
43
+ spec.add_development_dependency "standard", "~> 1.50"
44
+
45
+ # Post-install message
46
+ spec.post_install_message = "mcpeasy installed! Run 'mcpz setup' to configure."
47
+ end
metadata ADDED
@@ -0,0 +1,191 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mcpeasy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Joel Helbling
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: google-apis-calendar_v3
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.35'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.35'
26
+ - !ruby/object:Gem::Dependency
27
+ name: google-apis-drive_v3
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.45'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.45'
40
+ - !ruby/object:Gem::Dependency
41
+ name: googleauth
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.8'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.8'
54
+ - !ruby/object:Gem::Dependency
55
+ name: slack-ruby-client
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.1'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.1'
68
+ - !ruby/object:Gem::Dependency
69
+ name: webrick
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.8'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.8'
82
+ - !ruby/object:Gem::Dependency
83
+ name: thor
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.3'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.3'
96
+ - !ruby/object:Gem::Dependency
97
+ name: mother
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.1'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.1'
110
+ - !ruby/object:Gem::Dependency
111
+ name: standard
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '1.50'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '1.50'
124
+ description: mcpeasy, LM squeezy - Easy-to-use MCP servers for Google Calendar, Google
125
+ Drive, Google Meet, and Slack
126
+ email:
127
+ - joel@joelhelbling.com
128
+ executables:
129
+ - mcpz
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - ".claude/settings.json"
134
+ - ".claudeignore"
135
+ - ".envrc"
136
+ - ".mcp.json"
137
+ - CLAUDE.md
138
+ - README.md
139
+ - bin/mcpz
140
+ - env.template
141
+ - ext/setup.rb
142
+ - lib/mcpeasy.rb
143
+ - lib/mcpeasy/cli.rb
144
+ - lib/mcpeasy/config.rb
145
+ - lib/mcpeasy/setup.rb
146
+ - lib/mcpeasy/version.rb
147
+ - lib/utilities/_google/auth_server.rb
148
+ - lib/utilities/gcal/README.md
149
+ - lib/utilities/gcal/cli.rb
150
+ - lib/utilities/gcal/gcal_tool.rb
151
+ - lib/utilities/gcal/mcp.rb
152
+ - lib/utilities/gdrive/README.md
153
+ - lib/utilities/gdrive/cli.rb
154
+ - lib/utilities/gdrive/gdrive_tool.rb
155
+ - lib/utilities/gdrive/mcp.rb
156
+ - lib/utilities/gmeet/README.md
157
+ - lib/utilities/gmeet/cli.rb
158
+ - lib/utilities/gmeet/gmeet_tool.rb
159
+ - lib/utilities/gmeet/mcp.rb
160
+ - lib/utilities/slack/README.md
161
+ - lib/utilities/slack/cli.rb
162
+ - lib/utilities/slack/mcp.rb
163
+ - lib/utilities/slack/slack_tool.rb
164
+ - logs/.keep
165
+ - mcpeasy.gemspec
166
+ homepage: https://github.com/joelhelbling/mcpeasy
167
+ licenses:
168
+ - MIT
169
+ metadata:
170
+ allowed_push_host: https://rubygems.org
171
+ homepage_uri: https://github.com/joelhelbling/mcpeasy
172
+ source_code_uri: https://github.com/joelhelbling/mcpeasy
173
+ post_install_message: mcpeasy installed! Run 'mcpz setup' to configure.
174
+ rdoc_options: []
175
+ require_paths:
176
+ - lib
177
+ required_ruby_version: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: 3.0.0
182
+ required_rubygems_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ requirements: []
188
+ rubygems_version: 3.6.7
189
+ specification_version: 4
190
+ summary: MCP servers made easy
191
+ test_files: []