mewmew 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 769d8c200467f6133c57ff947521b1a73be13d273c182904b8ba5b51d448f551
4
+ data.tar.gz: f39f7bceb745991663d71ef9e3e69a924bb978cd2108c06440fdc22f98628c1a
5
+ SHA512:
6
+ metadata.gz: 6b1fe4ae16186be03d19c9fb2af9e1ad63284ffb21eb9b68ad51791a54318f99b53c5116760b21ed3803e76a1f1d0704ec8e1ffd711d9c03c9e98deafdb72292
7
+ data.tar.gz: 61322d6cbffd4aca03bd8c4919b28c643443381d0edb992f686f7d76dae3ad51856073cc39da2c598876f954b3fe000a69558290814c47ef712476336e72aeda
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-08-28
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Martin Emde
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # Mewmew
2
+
3
+ Just add Mewmew to make any Thor CLI into an MCP server.
4
+
5
+ Mewmew automatically converts Thor CLI commands into MCP (Model Context Protocol) tools that can be used by AI assistants.
6
+
7
+ ## Installation
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem 'mewmew'
13
+ ```
14
+
15
+ or add to a gemspec for a command that already uses Thor.
16
+
17
+ ## Usage
18
+
19
+ ### 1. Include Mewmew in your Thor class
20
+
21
+ Simply add `include Mewmew` to your Thor CLI class:
22
+
23
+ ```ruby
24
+ require 'thor'
25
+ require 'mewmew'
26
+
27
+ class MyCli < Thor
28
+ include Mewmew # MCP command is automatically added!
29
+
30
+ # Optional: Customize MCP server options
31
+ # add_mcp_command(name: "my_custom_name", version: "2.0.0")
32
+
33
+ desc "hello NAME", "Say hello to someone"
34
+ def hello(name)
35
+ puts "Hello, #{name}!"
36
+ end
37
+
38
+ desc "greet", "Greet the world"
39
+ method_option :style, type: :string, default: "normal", desc: "Greeting style"
40
+ def greet
41
+ style = options[:style]
42
+ case style
43
+ when "loud"
44
+ puts "HELLO WORLD!"
45
+ when "quiet"
46
+ puts "hello world..."
47
+ else
48
+ puts "Hello world!"
49
+ end
50
+ end
51
+ end
52
+ ```
53
+
54
+ ### 2. Start the MCP server
55
+
56
+ Run your CLI with the `mcp` command:
57
+
58
+ ```bash
59
+ ruby my_cli mcp
60
+ ```
61
+
62
+ ## Cursor Integration
63
+
64
+ To use your CLI with Cursor:
65
+
66
+ 1. **Configure MCP in Cursor**: Add this to your `.cursor/mcp.json`:
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "my_cli": {
72
+ "command": "/path/to/ruby",
73
+ "args": ["/path/to/your/my_cli.rb", "mcp"]
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ 2. **Restart Cursor** to pick up the MCP configuration
80
+
81
+ 3. **Use in Cursor**: Your CLI commands will now be available as tools that Cursor can call automatically!
82
+
83
+ ## Example
84
+
85
+ Check out the complete example in `examples/example_cli.rb` which creates a Wikipedia lookup CLI that can be used with Cursor.
86
+
87
+ ## Features
88
+
89
+ - šŸš€ **Automatic detection**: Just `include Mewmew` in any Thor class
90
+ - ⚔ **Zero configuration**: MCP tools are automatically added - no extra steps needed
91
+ - šŸ› ļø **Works with existing Thor commands out of the box**
92
+ - šŸ“ **Full option support**: Method options, descriptions, and arguments are preserved
93
+ - šŸ”„ **Live updates**: Changes to your CLI are immediately available to Cursor
94
+
95
+ ## Requirements
96
+
97
+ - Ruby 3.0+
98
+ - Thor gem
99
+ - MCP gem (installed automatically)
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "thor"
6
+ rescue LoadError
7
+ warn "This example requires the 'thor' gem. Install with: gem install thor"
8
+ exit 1
9
+ end
10
+
11
+ require "net/http"
12
+ require "json"
13
+ require "uri"
14
+
15
+ # Load mewmew for MCP integration
16
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
17
+ require "mewmew"
18
+
19
+ class McuCli < Thor
20
+ include Mewmew
21
+
22
+ # MCP command is automatically added when Mewmew is included!
23
+ # You can optionally customize it: add_mcp_command(name: "mcu_cli", version: "1.0.0")
24
+ desc "lookup [QUERY]", "Lookup a movie/comic item on Wikipedia and print a summary"
25
+ method_option :lang, type: :string, default: "en", desc: "Wikipedia language code"
26
+ def lookup(query = nil)
27
+ lang = options[:lang]
28
+ normalized_query = normalize_query(query)
29
+ title = find_best_title(normalized_query, lang) || normalized_query
30
+ summary = fetch_summary(title, lang)
31
+
32
+ if summary && !summary.strip.empty?
33
+ say "Title: #{title}", :green
34
+ say summary
35
+ else
36
+ say "No summary found for '#{query}'.", :red
37
+ end
38
+ end
39
+
40
+ desc "hammer", "Show info about Thor's hammer (Mjolnir) a.k.a. 'Mew Mew'"
41
+ method_option :lang, type: :string, default: "en", desc: "Wikipedia language code"
42
+ def hammer
43
+ lookup("Mjolnir")
44
+ end
45
+
46
+ no_commands do
47
+ def normalize_query(q)
48
+ return "Mjolnir" if q.nil? || q.strip.empty?
49
+ key = q.to_s.strip.downcase
50
+ case key
51
+ when "mew mew", "meow meow", "mewmew", "meowmeow", "thor's hammer", "thors hammer"
52
+ "Mjolnir"
53
+ when "mjolnir", "mjƶlner", "mjƶllnir", "mjolnir (comics)", "mjƶlnir"
54
+ "Mjolnir"
55
+ else
56
+ q
57
+ end
58
+ end
59
+
60
+ def find_best_title(query, lang)
61
+ # Use MediaWiki opensearch to find the closest matching title
62
+ uri = URI("https://#{lang}.wikipedia.org/w/api.php")
63
+ uri.query = URI.encode_www_form(
64
+ action: "opensearch",
65
+ format: "json",
66
+ search: query,
67
+ limit: 5,
68
+ namespace: 0
69
+ )
70
+ data = fetch_json(uri)
71
+ return nil unless data.is_a?(Array) && data[1].is_a?(Array) && data[1].any?
72
+
73
+ titles = data[1]
74
+ descriptions = data[2] || []
75
+
76
+ ranked = titles.each_with_index.sort_by do |title, i|
77
+ desc = descriptions[i].to_s.downcase
78
+ score = 0
79
+ score -= i # earlier results get higher priority
80
+ score += 3 if title.downcase.include?("mjolnir") || title.downcase.include?("mjƶlnir")
81
+ score += 2 if desc.include?("marvel")
82
+ score += 1 if desc.include?("thor")
83
+ -score
84
+ end
85
+
86
+ ranked.first&.first
87
+ rescue StandardError
88
+ nil
89
+ end
90
+
91
+ def fetch_summary(title, lang)
92
+ # Use REST summary endpoint which follows redirects and returns plaintext extract
93
+ # IMPORTANT: Title is a PATH segment, not a query param. Spaces should be underscores, not '+'.
94
+ slug = title.to_s.strip.tr(" ", "_")
95
+ uri = URI("https://#{lang}.wikipedia.org/api/rest_v1/page/summary/#{slug}")
96
+ data = fetch_json(uri)
97
+ data && (data["extract"] || data["description"])
98
+ rescue StandardError
99
+ nil
100
+ end
101
+
102
+ def fetch_json(uri)
103
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
104
+ req = Net::HTTP::Get.new(uri)
105
+ req["User-Agent"] = "mewmew-example-cli/1.0 (https://github.com/martinemde/mewmew)"
106
+ http.request(req)
107
+ end
108
+ return nil unless response.is_a?(Net::HTTPSuccess)
109
+ JSON.parse(response.body)
110
+ end
111
+ end
112
+
113
+ def self.exit_on_failure?
114
+ true
115
+ end
116
+ end
117
+
118
+ McuCli.start(ARGV)
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+ cd "$(dirname "$0")/.."
3
+ exec bundle exec ruby examples/example_cli.rb mcp
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "mcp"
5
+ require "mcp/server/transports/stdio_transport"
6
+ rescue LoadError
7
+ raise LoadError, "MCP gem is required. Add 'mcp' to your Gemfile."
8
+ end
9
+
10
+ module Mewmew
11
+ # Integrates Thor CLI with MCP server
12
+ class McpIntegration
13
+ attr_reader :thor_class, :server_options
14
+
15
+ def initialize(thor_class, **server_options)
16
+ @thor_class = thor_class
17
+ @server_options = server_options
18
+ end
19
+
20
+ # Start the MCP stdio server
21
+ def serve
22
+ server = create_server
23
+ transport = MCP::Server::Transports::StdioTransport.new(server)
24
+
25
+ # Trap signals for graceful shutdown
26
+ %w[INT TERM].each do |signal|
27
+ Signal.trap(signal) do
28
+ warn "\nShutting down MCP server..."
29
+ transport.close
30
+ exit(0)
31
+ end
32
+ end
33
+
34
+ transport.open
35
+ end
36
+
37
+ # Create an MCP server with Thor tools
38
+ def create_server
39
+ tools = ThorIntrospector.new(thor_class).to_mcp_tools
40
+
41
+ MCP::Server.new(
42
+ name: server_name,
43
+ version: server_version,
44
+ tools: tools,
45
+ **server_options
46
+ )
47
+ end
48
+
49
+ private
50
+
51
+ def server_name
52
+ server_options[:name] || thor_class_name.downcase
53
+ end
54
+
55
+ def server_version
56
+ server_options[:version] || "1.0.0"
57
+ end
58
+
59
+ def thor_class_name
60
+ thor_class.name&.split("::")&.last || "ThorCLI"
61
+ end
62
+ end
63
+
64
+ # Module to be included in Thor classes to add MCP functionality
65
+ module ThorMcpExtension
66
+ def self.included(base)
67
+ base.extend(ClassMethods)
68
+ # Automatically add the MCP command with default options
69
+ base.add_mcp_command
70
+ end
71
+
72
+ module ClassMethods
73
+ # Add the mcp command to the Thor class
74
+ def add_mcp_command(**server_options)
75
+ desc "mcp", "Start MCP (Model Context Protocol) server to expose CLI commands as tools"
76
+ define_method :mcp do
77
+ Mewmew.serve_cli(self.class, **server_options)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ begin
6
+ require "mcp"
7
+ rescue LoadError
8
+ raise LoadError, "MCP gem is required. Add 'mcp' to your Gemfile."
9
+ end
10
+
11
+ module Mewmew
12
+ # Introspects Thor CLI classes and extracts command information
13
+ class ThorIntrospector
14
+ attr_reader :thor_class
15
+
16
+ def initialize(thor_class)
17
+ @thor_class = thor_class
18
+ validate_thor_class!
19
+ end
20
+
21
+ # Get all commands from the Thor class
22
+ def commands
23
+ @commands ||= thor_class.commands.reject { |name, _| %w[help mcp].include?(name) }
24
+ end
25
+
26
+ # Convert Thor commands to MCP tools
27
+ def to_mcp_tools
28
+ commands.map do |name, command|
29
+ create_tool_class(name, command, thor_class)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def create_tool_class(command_name, thor_command, thor_class)
36
+ # Dynamically create a tool class that inherits from MCP::Tool
37
+ Class.new(MCP::Tool) do
38
+ # Set the tool metadata
39
+ tool_name command_name
40
+ description thor_command.description || "Execute the #{command_name} command"
41
+
42
+ # Build input schema from method signature and options
43
+ schema_properties = {}
44
+ schema_required = []
45
+
46
+ # Handle command arguments from method signature
47
+ method = thor_class.instance_method(command_name.to_sym)
48
+ method.parameters.each do |param_type, param_name|
49
+ case param_type
50
+ when :req
51
+ schema_properties[param_name] = {
52
+ type: "string",
53
+ description: "#{param_name.to_s.capitalize} argument"
54
+ }
55
+ schema_required << param_name.to_s
56
+ when :opt
57
+ schema_properties[param_name] = {
58
+ type: "string",
59
+ description: "#{param_name.to_s.capitalize} argument (optional)"
60
+ }
61
+ when :rest
62
+ schema_properties[param_name] = {
63
+ type: "array",
64
+ items: { type: "string" },
65
+ description: "Additional #{param_name.to_s.capitalize} arguments"
66
+ }
67
+ end
68
+ end
69
+
70
+ # Handle command options
71
+ if thor_command.options
72
+ thor_command.options.each do |option_name, option|
73
+ next if option_name == :help
74
+
75
+ schema = {
76
+ description: option.description || "#{option.name.to_s.gsub('_', ' ').capitalize} option"
77
+ }
78
+
79
+ case option.type
80
+ when :boolean
81
+ schema[:type] = "boolean"
82
+ schema[:default] = option.default if !option.default.nil?
83
+ when :numeric
84
+ schema[:type] = "number"
85
+ schema[:default] = option.default if !option.default.nil?
86
+ when :array
87
+ schema[:type] = "array"
88
+ schema[:items] = { type: "string" }
89
+ schema[:default] = option.default if option.default
90
+ else
91
+ schema[:type] = "string"
92
+ schema[:default] = option.default if option.default
93
+ end
94
+
95
+ schema_properties[option_name] = schema
96
+ end
97
+ end
98
+
99
+ input_schema(properties: schema_properties, required: schema_required)
100
+
101
+ # Define the class method to execute the command
102
+ define_singleton_method :call do |server_context: nil, **args|
103
+ begin
104
+ # Create a new instance of the Thor class
105
+ thor_instance = thor_class.new
106
+
107
+ # Extract arguments and options
108
+ method = thor_class.instance_method(command_name.to_sym)
109
+ arg_names = method.parameters
110
+ .select { |type, _| [:req, :opt].include?(type) }
111
+ .map { |_, name| name }
112
+
113
+ command_args = []
114
+ options = {}
115
+
116
+ args.each do |key, value|
117
+ key_sym = key.to_sym
118
+ if arg_names.include?(key_sym)
119
+ arg_index = arg_names.index(key_sym)
120
+ command_args[arg_index] = value
121
+ else
122
+ options[key_sym] = value
123
+ end
124
+ end
125
+
126
+ # Fill in missing arguments with nil
127
+ command_args.fill(nil, command_args.length, arg_names.length - command_args.length) if arg_names.any?
128
+
129
+ # Set up Thor instance with options
130
+ thor_instance.options = thor_instance.options.merge(options) if options.any?
131
+
132
+ # Capture output
133
+ original_stdout = $stdout
134
+ stdout_capture = StringIO.new
135
+
136
+ # Only capture stdout, leave stderr for debug output
137
+ $stdout = stdout_capture
138
+
139
+ begin
140
+ thor_instance.send(command_name, *command_args.compact)
141
+
142
+ stdout_content = stdout_capture.string
143
+ output = stdout_content.empty? ? "Command executed successfully" : stdout_content
144
+ ensure
145
+ $stdout = original_stdout
146
+ end
147
+
148
+ MCP::Tool::Response.new([{
149
+ type: "text",
150
+ text: output
151
+ }])
152
+ rescue => e
153
+ MCP::Tool::Response.new([{
154
+ type: "text",
155
+ text: "Error executing command '#{command_name}': #{e.message}"
156
+ }], true)
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ private
163
+
164
+ def validate_thor_class!
165
+ # Check if the class inherits from any Thor-like class
166
+ unless thor_like_class?(thor_class)
167
+ raise Error, "Class must inherit from Thor or a Thor-like class. Got: #{thor_class} with superclass: #{thor_class.superclass}"
168
+ end
169
+ end
170
+
171
+ def thor_like_class?(klass)
172
+ return false unless klass.is_a?(Class)
173
+
174
+ # Check the inheritance chain for Thor-like classes
175
+ current_class = klass
176
+ while current_class && current_class != Object
177
+ parent = current_class.superclass
178
+ if parent && (parent.name == "Thor" || parent.name&.end_with?("::Thor"))
179
+ return true
180
+ end
181
+ current_class = parent
182
+ end
183
+
184
+ false
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mewmew
4
+ VERSION = "0.1.0"
5
+ end
data/lib/mewmew.rb ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mewmew/version"
4
+ require_relative "mewmew/thor_introspector"
5
+ require_relative "mewmew/mcp_integration"
6
+
7
+ module Mewmew
8
+ class Error < StandardError; end
9
+
10
+ # Auto-detect Thor classes and include MCP functionality
11
+ def self.included(base)
12
+ # Check if the base class is a Thor class by looking for Thor inheritance
13
+ if thor_class?(base)
14
+ base.include(Mewmew::ThorMcpExtension)
15
+ end
16
+ end
17
+
18
+ class << self
19
+ # Convert a Thor CLI class to MCP tools and start a server
20
+ def serve_cli(thor_class, **server_options)
21
+ McpIntegration.new(thor_class, **server_options).serve
22
+ end
23
+
24
+ # Get MCP tools for a Thor CLI class without starting server
25
+ def tools_for_cli(thor_class)
26
+ ThorIntrospector.new(thor_class).to_mcp_tools
27
+ end
28
+
29
+ private
30
+
31
+ # Check if a class is a Thor class
32
+ def thor_class?(klass)
33
+ # Check if it's a subclass of Thor or includes Thor::Base
34
+ return true if defined?(::Thor) && klass <= ::Thor
35
+ return true if defined?(::Thor::Base) && klass.include?(::Thor::Base)
36
+ false
37
+ rescue
38
+ false
39
+ end
40
+ end
41
+ end
data/sig/mewmew.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Mewmew
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "open3"
6
+
7
+ class MCPIntegrationTest
8
+ attr_reader :test_results, :total_tests, :passed_tests
9
+
10
+ def initialize
11
+ @test_results = []
12
+ @total_tests = 0
13
+ @passed_tests = 0
14
+ end
15
+
16
+ def run_all_tests
17
+ puts "🧪 Running MCP Integration Tests"
18
+ puts "=" * 50
19
+
20
+ test_tools_list
21
+ test_lookup_with_query
22
+ test_lookup_with_options
23
+ test_hammer_command
24
+ test_invalid_tool
25
+ test_missing_args
26
+ test_boolean_option
27
+
28
+ print_summary
29
+ exit(@passed_tests == @total_tests ? 0 : 1)
30
+ end
31
+
32
+ private
33
+
34
+ def test_tools_list
35
+ test_name = "Tools List"
36
+ puts "\nšŸ” Testing: #{test_name}"
37
+
38
+ result = run_mcp_command("tools/list")
39
+
40
+ if result[:success] && result[:data]
41
+ tools = result[:data]["tools"]
42
+ expected_tools = %w[lookup hammer]
43
+ actual_tools = tools.map { |t| t["name"] }.sort
44
+
45
+ if actual_tools == expected_tools.sort
46
+ assert_pass(test_name, "Found expected tools: #{actual_tools.join(', ')}")
47
+
48
+ # Check tool schemas
49
+ lookup_tool = tools.find { |t| t["name"] == "lookup" }
50
+ if lookup_tool && lookup_tool["inputSchema"]["properties"]["query"]
51
+ assert_pass("#{test_name} - Schema", "Lookup tool has query parameter")
52
+ else
53
+ assert_fail("#{test_name} - Schema", "Lookup tool missing query parameter")
54
+ end
55
+ else
56
+ assert_fail(test_name, "Expected tools: #{expected_tools}, got: #{actual_tools}")
57
+ end
58
+ else
59
+ assert_fail(test_name, "Failed to get tools list: #{result[:error]}")
60
+ end
61
+ end
62
+
63
+ def test_lookup_with_query
64
+ test_name = "Lookup with Query"
65
+ puts "\nšŸ” Testing: #{test_name}"
66
+
67
+ result = run_mcp_command("tools/call", tool_name: "lookup", args: { query: "mjolnir" })
68
+
69
+ if result[:success] && result[:data]
70
+ content = result[:data]["content"]
71
+ if content && content[0] && content[0]["text"]
72
+ text = content[0]["text"]
73
+ # The CLI should return some result (even if it's "No summary found")
74
+ if text.length > 0
75
+ assert_pass(test_name, "Returned text: #{text.strip}")
76
+ else
77
+ assert_fail(test_name, "Empty response text")
78
+ end
79
+ else
80
+ assert_fail(test_name, "No text content in response")
81
+ end
82
+ else
83
+ assert_fail(test_name, "Tool call failed: #{result[:error]}")
84
+ end
85
+ end
86
+
87
+ def test_lookup_with_options
88
+ test_name = "Lookup with Language Option"
89
+ puts "\nšŸ” Testing: #{test_name}"
90
+
91
+ result = run_mcp_command("tools/call", tool_name: "lookup", args: { query: "thor", lang: "en" })
92
+
93
+ if result[:success] && result[:data]
94
+ content = result[:data]["content"]
95
+ if content && content[0] && content[0]["text"]
96
+ text = content[0]["text"]
97
+ # Should execute without error (even if no results found)
98
+ assert_pass(test_name, "Executed with lang option: #{text.strip}")
99
+ else
100
+ assert_fail(test_name, "No text content in response")
101
+ end
102
+ else
103
+ assert_fail(test_name, "Tool call failed: #{result[:error]}")
104
+ end
105
+ end
106
+
107
+ def test_hammer_command
108
+ test_name = "Hammer Command"
109
+ puts "\nšŸ” Testing: #{test_name}"
110
+
111
+ result = run_mcp_command("tools/call", tool_name: "hammer")
112
+
113
+ if result[:success] && result[:data]
114
+ content = result[:data]["content"]
115
+ if content && content[0] && content[0]["text"]
116
+ text = content[0]["text"]
117
+ # Hammer command calls lookup("Mjolnir") internally
118
+ assert_pass(test_name, "Hammer command executed: #{text.strip}")
119
+ else
120
+ assert_fail(test_name, "No text content in response")
121
+ end
122
+ else
123
+ assert_fail(test_name, "Tool call failed: #{result[:error]}")
124
+ end
125
+ end
126
+
127
+ def test_invalid_tool
128
+ test_name = "Invalid Tool Name"
129
+ puts "\nšŸ” Testing: #{test_name}"
130
+
131
+ result = run_mcp_command("tools/call", tool_name: "nonexistent")
132
+
133
+ if result[:success]
134
+ assert_fail(test_name, "Should have failed for invalid tool name")
135
+ else
136
+ # Should get an error for invalid tool
137
+ assert_pass(test_name, "Correctly rejected invalid tool: #{result[:error]}")
138
+ end
139
+ end
140
+
141
+ def test_missing_args
142
+ test_name = "Missing Arguments"
143
+ puts "\nšŸ” Testing: #{test_name}"
144
+
145
+ # Test calling lookup without query (it's optional but let's see what happens)
146
+ result = run_mcp_command("tools/call", tool_name: "lookup", args: {})
147
+
148
+ if result[:success] && result[:data]
149
+ content = result[:data]["content"]
150
+ if content && content[0] && content[0]["text"]
151
+ # Should work with no query (optional parameter)
152
+ assert_pass(test_name, "Handled missing optional parameter correctly")
153
+ else
154
+ assert_fail(test_name, "No content returned")
155
+ end
156
+ else
157
+ assert_fail(test_name, "Unexpected failure: #{result[:error]}")
158
+ end
159
+ end
160
+
161
+ def test_boolean_option
162
+ test_name = "Boolean Option"
163
+ puts "\nšŸ” Testing: #{test_name}"
164
+
165
+ # Test with lang option (string) - should work
166
+ result = run_mcp_command("tools/call", tool_name: "lookup", args: { query: "test", lang: "fr" })
167
+
168
+ if result[:success] && result[:data]
169
+ assert_pass(test_name, "Boolean/string options handled correctly")
170
+ else
171
+ assert_fail(test_name, "Failed with options: #{result[:error]}")
172
+ end
173
+ end
174
+
175
+ def run_mcp_command(method, tool_name: nil, args: {})
176
+ cmd_parts = [
177
+ "npx", "@modelcontextprotocol/inspector",
178
+ "--cli", "./examples/mcp_server.sh",
179
+ "--method", method
180
+ ]
181
+
182
+ if tool_name
183
+ cmd_parts += ["--tool-name", tool_name]
184
+ end
185
+
186
+ args.each do |key, value|
187
+ cmd_parts += ["--tool-arg", "#{key}=#{value}"]
188
+ end
189
+
190
+ puts " šŸ’» Running: #{cmd_parts.join(' ')}"
191
+
192
+ stdout, stderr, status = Open3.capture3(*cmd_parts)
193
+
194
+ if status.success?
195
+ begin
196
+ data = JSON.parse(stdout)
197
+ { success: true, data: data }
198
+ rescue JSON::ParserError => e
199
+ { success: false, error: "JSON parse error: #{e.message}, output: #{stdout}" }
200
+ end
201
+ else
202
+ { success: false, error: "Command failed (#{status.exitstatus}): #{stderr}" }
203
+ end
204
+ rescue => e
205
+ { success: false, error: "Exception: #{e.message}" }
206
+ end
207
+
208
+ def assert_pass(test_name, message)
209
+ @total_tests += 1
210
+ @passed_tests += 1
211
+ @test_results << { name: test_name, status: :pass, message: message }
212
+ puts " āœ… PASS: #{message}"
213
+ end
214
+
215
+ def assert_fail(test_name, message)
216
+ @total_tests += 1
217
+ @test_results << { name: test_name, status: :fail, message: message }
218
+ puts " āŒ FAIL: #{message}"
219
+ end
220
+
221
+ def print_summary
222
+ puts "\n" + "=" * 50
223
+ puts "šŸ“Š TEST SUMMARY"
224
+ puts "=" * 50
225
+
226
+ @test_results.each do |result|
227
+ icon = result[:status] == :pass ? "āœ…" : "āŒ"
228
+ puts "#{icon} #{result[:name]}: #{result[:message]}"
229
+ end
230
+
231
+ puts "\nšŸ“ˆ Results: #{@passed_tests}/#{@total_tests} tests passed"
232
+
233
+ if @passed_tests == @total_tests
234
+ puts "šŸŽ‰ All tests passed!"
235
+ else
236
+ puts "šŸ’„ #{@total_tests - @passed_tests} test(s) failed"
237
+ end
238
+ end
239
+ end
240
+
241
+ # Run the tests if this file is executed directly
242
+ if __FILE__ == $0
243
+ MCPIntegrationTest.new.run_all_tests
244
+ end
data/test/run_tests.sh ADDED
@@ -0,0 +1,36 @@
1
+ #!/bin/bash
2
+ # Test runner for MCP integration tests
3
+
4
+ set -e
5
+
6
+ echo "🧪 MCP Integration Test Runner"
7
+ echo "=============================="
8
+
9
+ # Change to project root
10
+ cd "$(dirname "$0")/.."
11
+
12
+ echo "šŸ“ Working directory: $(pwd)"
13
+
14
+ # Ensure bundle is available
15
+ echo "šŸ“¦ Checking bundle..."
16
+ if ! command -v bundle &> /dev/null; then
17
+ echo "āŒ Bundle not found. Please install bundler."
18
+ exit 1
19
+ fi
20
+
21
+ # Install dependencies
22
+ echo "šŸ“„ Installing dependencies..."
23
+ bundle install --quiet
24
+
25
+ # Run smoke test first
26
+ echo ""
27
+ echo "šŸ”„ Running smoke test..."
28
+ bundle exec ruby test/smoke_test.rb
29
+
30
+ # Run full integration tests
31
+ echo ""
32
+ echo "šŸš€ Running full integration tests..."
33
+ bundle exec ruby test/integration_test.rb
34
+
35
+ echo ""
36
+ echo "✨ All tests completed successfully!"
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "open3"
6
+
7
+ puts "šŸ”„ MCP Smoke Test"
8
+ puts "=" * 30
9
+
10
+ # Test 1: Basic server startup and tools list
11
+ puts "\n1. Testing basic server startup..."
12
+
13
+ stdout, stderr, status = Open3.capture3(
14
+ "npx", "@modelcontextprotocol/inspector",
15
+ "--cli", "./examples/mcp_server.sh",
16
+ "--method", "tools/list"
17
+ )
18
+
19
+ if status.success?
20
+ begin
21
+ data = JSON.parse(stdout)
22
+ tools = data["tools"]
23
+ puts "āœ… Server started successfully"
24
+ puts "āœ… Found #{tools.length} tools: #{tools.map { |t| t['name'] }.join(', ')}"
25
+ rescue JSON::ParserError => e
26
+ puts "āŒ JSON parse error: #{e.message}"
27
+ puts "Raw output: #{stdout}"
28
+ exit 1
29
+ end
30
+ else
31
+ puts "āŒ Server failed to start"
32
+ puts "stderr: #{stderr}"
33
+ puts "stdout: #{stdout}"
34
+ exit 1
35
+ end
36
+
37
+ # Test 2: Simple tool call
38
+ puts "\n2. Testing simple tool call..."
39
+
40
+ stdout, stderr, status = Open3.capture3(
41
+ "npx", "@modelcontextprotocol/inspector",
42
+ "--cli", "./examples/mcp_server.sh",
43
+ "--method", "tools/call",
44
+ "--tool-name", "hammer"
45
+ )
46
+
47
+ if status.success?
48
+ begin
49
+ data = JSON.parse(stdout)
50
+ content = data["content"]
51
+ if content && content[0] && content[0]["text"]
52
+ puts "āœ… Tool call successful"
53
+ puts "āœ… Response: #{content[0]['text'].strip[0..100]}..."
54
+ else
55
+ puts "āŒ No content in response"
56
+ exit 1
57
+ end
58
+ rescue JSON::ParserError => e
59
+ puts "āŒ JSON parse error: #{e.message}"
60
+ exit 1
61
+ end
62
+ else
63
+ puts "āŒ Tool call failed"
64
+ puts "stderr: #{stderr}"
65
+ exit 1
66
+ end
67
+
68
+ puts "\nšŸŽ‰ Smoke test passed! Ready for full integration tests."
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mewmew
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Martin Emde
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: mcp
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: thor
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ description: Load any Thor CLI directly as a StdIO MCP server
41
+ email:
42
+ - me@martinemde.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - CHANGELOG.md
48
+ - LICENSE.txt
49
+ - README.md
50
+ - Rakefile
51
+ - examples/example_cli.rb
52
+ - examples/mcp_server.sh
53
+ - lib/mewmew.rb
54
+ - lib/mewmew/mcp_integration.rb
55
+ - lib/mewmew/thor_introspector.rb
56
+ - lib/mewmew/version.rb
57
+ - sig/mewmew.rbs
58
+ - test/integration_test.rb
59
+ - test/run_tests.sh
60
+ - test/smoke_test.rb
61
+ homepage: https://github.com/martinemde/mewmew
62
+ licenses:
63
+ - MIT
64
+ metadata:
65
+ allowed_push_host: https://rubygems.org
66
+ homepage_uri: https://github.com/martinemde/mewmew
67
+ source_code_uri: https://github.com/martinemde/mewmew
68
+ changelog_uri: https://github.com/martinemde/mewmew/blob/main/CHANGELOG.md
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 3.2.0
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubygems_version: 3.6.9
84
+ specification_version: 4
85
+ summary: Make any thor command an MCP
86
+ test_files: []