mcp_cli 1.0.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: bcbfec231c47b8e10fe71e552a116827b39b2c165f852c1d64983db8efcfa514
4
+ data.tar.gz: c03bc9fa15d96dff393bb31bd50afe9a91f5808a503ac7ba913398ce15eb8ca7
5
+ SHA512:
6
+ metadata.gz: 4fcd6595e7045f359ec79fb5a3a24322badf17f17971ce2348b4103fb69fdca118214fdf069ef749204ad68f13233911062433299ed104dedc9e3934604cb0d0
7
+ data.tar.gz: 637a802a9efac0be603195855d070797dba4bd903ddc784faec8d569989a1fb4824e015eb57c938b4350cd9acd11cf71d5eb97604c90bf27090b6a83d0d6b581
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2025-10-30
9
+
10
+ ### Added
11
+ - Initial release of MCP CLI
12
+ - Support for stdio-based MCP servers
13
+ - Support for HTTP-based MCP servers
14
+ - Commands: list, tools, prompts, resources, call, prompt, info, version, config
15
+ - Auto-discovery of config files (~/.claude.json, ~/.cursor/mcp.json, ~/.vscode/mcp.json)
16
+ - Flexible argument parsing (JSON and flag-style)
17
+ - Zero runtime dependencies (stdlib only)
18
+ - Optimized for `gem exec` usage
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Josh Beckman
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # MCP CLI
2
+
3
+ A zero-dependency command-line interface for interacting with [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers. MCP enables AI assistants to securely connect to local and remote resources through a standardized protocol.
4
+
5
+ Perfect for developers who need to:
6
+ - Test MCP server implementations
7
+ - Debug server responses
8
+ - Integrate MCP servers into scripts and workflows
9
+ - Explore available tools and resources
10
+
11
+ Supports both stdio and HTTP transports with automatic configuration discovery.
12
+
13
+ ## Requirements
14
+
15
+ - Ruby 2.7 or higher
16
+ - RubyGems 3.0 or higher
17
+ - Compatible with macOS, Linux, and Windows
18
+
19
+ ## Quick Start (No Installation)
20
+
21
+ The fastest way to use MCP CLI is with `gem exec` - no installation required:
22
+
23
+ ```bash
24
+ gem exec mcp_cli list
25
+ gem exec mcp_cli tools my-server
26
+ gem exec mcp_cli call my-server my-tool --arg value
27
+ ```
28
+
29
+ This is perfect for trying out the tool or using it in scripts without adding dependencies.
30
+
31
+ ## Installation (Optional)
32
+
33
+ If you prefer to install the gem:
34
+
35
+ ```bash
36
+ gem install mcp_cli
37
+ ```
38
+
39
+ Then use it directly:
40
+
41
+ ```bash
42
+ mcp list
43
+ mcp tools my-server
44
+ ```
45
+
46
+ ## Configuration
47
+
48
+ MCP CLI looks for server configurations in these locations (in order):
49
+
50
+ 1. `~/.claude.json`
51
+ 2. `~/.cursor/mcp.json`
52
+ 3. `~/.vscode/mcp.json`
53
+
54
+ You can also specify a custom config file:
55
+
56
+ ```bash
57
+ gem exec mcp_cli --mcp-config /path/to/config.json list
58
+ ```
59
+
60
+ Or use shortcuts for default configs:
61
+
62
+ ```bash
63
+ gem exec mcp_cli --mcp-config claude list
64
+ gem exec mcp_cli --mcp-config cursor list
65
+ gem exec mcp_cli --mcp-config vscode list
66
+ ```
67
+
68
+ ### Configuration Format
69
+
70
+ Your config file should follow this structure:
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "my-server": {
76
+ "type": "stdio",
77
+ "command": "node",
78
+ "args": ["/path/to/server.js"],
79
+ "env": {
80
+ "API_KEY": "your-key"
81
+ }
82
+ },
83
+ "http-server": {
84
+ "type": "http",
85
+ "url": "https://example.com/mcp",
86
+ "headers": {
87
+ "Authorization": "Bearer token"
88
+ }
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ ## Usage
95
+
96
+ ### List Available Servers
97
+
98
+ ```bash
99
+ gem exec mcp_cli list
100
+ ```
101
+
102
+ ### List Tools
103
+
104
+ ```bash
105
+ # List all tools on a server
106
+ gem exec mcp_cli tools my-server
107
+
108
+ # Show details for a specific tool
109
+ gem exec mcp_cli tools my-server search_all
110
+ ```
111
+
112
+ ### Call a Tool
113
+
114
+ ```bash
115
+ # With JSON arguments
116
+ gem exec mcp_cli call my-server search_all '{"query": "example"}'
117
+
118
+ # With flag-style arguments
119
+ gem exec mcp_cli call my-server search_all --query example
120
+
121
+ # Boolean flags
122
+ gem exec mcp_cli call my-server sync --force
123
+ ```
124
+
125
+ ### List Prompts
126
+
127
+ ```bash
128
+ gem exec mcp_cli prompts my-server
129
+ ```
130
+
131
+ ### Get a Prompt
132
+
133
+ ```bash
134
+ gem exec mcp_cli prompt my-server explain '{"topic": "MCP servers"}'
135
+ ```
136
+
137
+ ### List Resources
138
+
139
+ ```bash
140
+ gem exec mcp_cli resources my-server
141
+ ```
142
+
143
+ ### Server Information
144
+
145
+ ```bash
146
+ # Full server info
147
+ gem exec mcp_cli info my-server
148
+
149
+ # Just the version
150
+ gem exec mcp_cli version my-server
151
+
152
+ # Show configuration
153
+ gem exec mcp_cli config my-server
154
+ ```
155
+
156
+ ### Options
157
+
158
+ ```bash
159
+ # Use a specific protocol version
160
+ gem exec mcp_cli --protocol-version 2025-06-18 tools my-server
161
+
162
+ # Use a custom config file
163
+ gem exec mcp_cli --mcp-config /path/to/config.json list
164
+
165
+ # Show help
166
+ gem exec mcp_cli --help
167
+
168
+ # Show version
169
+ gem exec mcp_cli --version
170
+ ```
171
+
172
+ ## Features
173
+
174
+ - **Zero dependencies** - Uses only Ruby standard library
175
+ - **Fast startup** - Minimal overhead for quick commands
176
+ - **Flexible arguments** - Supports both JSON and flag-style arguments
177
+ - **Multiple transports** - Works with stdio and HTTP MCP servers
178
+ - **Config auto-discovery** - Finds your existing MCP configurations
179
+
180
+ ## Troubleshooting
181
+
182
+ ### Server not found
183
+ Ensure your server name matches exactly what's in your config file. Server names are case-sensitive.
184
+
185
+ ### Connection timeout
186
+ For stdio servers, verify the command path exists and is executable. Check that all required dependencies are installed.
187
+
188
+ ### Authentication errors
189
+ Check that environment variables and headers are properly set in your config. For HTTP servers, ensure your authentication tokens are valid.
190
+
191
+ ### No config file found
192
+ MCP CLI looks for configs in `~/.claude.json`, `~/.cursor/mcp.json`, or `~/.vscode/mcp.json`. Create one of these files or specify a custom path with `--mcp-config`.
193
+
194
+ ## Development
195
+
196
+ ```bash
197
+ # Clone the repo
198
+ git clone https://github.com/joshbeckman/mcp_cli.git
199
+ cd mcp_cli
200
+
201
+ # Install dependencies (just development tools)
202
+ bundle install
203
+
204
+ # Run tests
205
+ bundle exec rspec
206
+
207
+ # Test locally with gem exec
208
+ gem exec -g mcp_cli.gemspec mcp list
209
+ ```
210
+
211
+ ## License
212
+
213
+ MIT
214
+
215
+ ## Contributing
216
+
217
+ Bug reports and pull requests welcome on GitHub at https://github.com/joshbeckman/mcp_cli.
data/exe/mcp ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'mcp_cli'
5
+
6
+ MCPCli::CLI.new.run(ARGV)
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+
6
+ module MCPCli
7
+ # Client for stdio-based MCP servers
8
+ class Client
9
+ def initialize(command, args, env, protocol_version = '2025-06-18')
10
+ @command = command
11
+ @args = args || []
12
+ @env = env
13
+ @protocol_version = protocol_version
14
+ @process = nil
15
+ @reader_thread = nil
16
+ @message_id = 0
17
+ @pending_requests = {}
18
+ @initialized = false
19
+ @server_info = nil
20
+ end
21
+
22
+ def list_tools
23
+ ensure_initialized
24
+ response = send_request('tools/list', {})
25
+ response['tools'] || []
26
+ end
27
+
28
+ def list_prompts
29
+ ensure_initialized
30
+ response = send_request('prompts/list', {})
31
+ response['prompts'] || []
32
+ end
33
+
34
+ def list_resources
35
+ ensure_initialized
36
+ response = send_request('resources/list', {})
37
+ response['resources'] || []
38
+ end
39
+
40
+ def call_tool(tool_name, arguments)
41
+ ensure_initialized
42
+ response = send_request('tools/call', {
43
+ name: tool_name,
44
+ arguments: arguments
45
+ })
46
+ response['content'] || response
47
+ end
48
+
49
+ def get_prompt(prompt_name, arguments)
50
+ ensure_initialized
51
+ response = send_request('prompts/get', {
52
+ name: prompt_name,
53
+ arguments: arguments
54
+ })
55
+ if response['messages']
56
+ response['messages'].map { |msg| msg['content']['text'] || msg['content'] }.join("\n\n")
57
+ else
58
+ response
59
+ end
60
+ end
61
+
62
+ def get_server_info
63
+ ensure_initialized
64
+ @server_info || {}
65
+ end
66
+
67
+ private
68
+
69
+ def ensure_initialized
70
+ return if @initialized
71
+
72
+ start_process
73
+ response = send_request('initialize', {
74
+ protocolVersion: @protocol_version,
75
+ capabilities: {
76
+ roots: { listChanged: true },
77
+ sampling: {}
78
+ },
79
+ clientInfo: {
80
+ name: 'MCP CLI',
81
+ version: MCPCli::VERSION
82
+ }
83
+ })
84
+
85
+ @server_info = response['serverInfo'] if response['serverInfo']
86
+ @initialized = true
87
+ end
88
+
89
+ def start_process
90
+ cmd = [@command] + @args
91
+ @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(@env, *cmd)
92
+
93
+ @reader_thread = Thread.new do
94
+ while (line = @stdout.gets)
95
+ handle_message(JSON.parse(line))
96
+ end
97
+ rescue StandardError => e
98
+ puts "Reader thread error: #{e.message}"
99
+ end
100
+ end
101
+
102
+ def send_request(method, params)
103
+ message_id = next_message_id
104
+ message = {
105
+ jsonrpc: '2.0',
106
+ id: message_id,
107
+ method: method,
108
+ params: params
109
+ }
110
+
111
+ @stdin.puts(JSON.generate(message))
112
+ @stdin.flush
113
+
114
+ wait_for_response(message_id)
115
+ end
116
+
117
+ def handle_message(message)
118
+ return unless message['id'] && @pending_requests[message['id']]
119
+
120
+ @pending_requests[message['id']][:response] = message
121
+ @pending_requests[message['id']][:condition].signal
122
+ end
123
+
124
+ def wait_for_response(message_id)
125
+ mutex = Mutex.new
126
+ condition = ConditionVariable.new
127
+ @pending_requests[message_id] = { condition: condition, response: nil }
128
+
129
+ mutex.synchronize do
130
+ condition.wait(mutex, 30)
131
+ end
132
+
133
+ response = @pending_requests[message_id][:response]
134
+ @pending_requests.delete(message_id)
135
+
136
+ if response.nil?
137
+ raise 'Timeout waiting for response'
138
+ elsif response['error']
139
+ raise "MCP Error: #{response['error']['message']}"
140
+ end
141
+
142
+ response['result']
143
+ end
144
+
145
+ def next_message_id
146
+ @message_id += 1
147
+ end
148
+
149
+ def cleanup
150
+ @reader_thread&.kill
151
+ @stdin&.close
152
+ @stdout&.close
153
+ @stderr&.close
154
+ @wait_thread&.kill
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+ require 'securerandom'
7
+
8
+ module MCPCli
9
+ # Client for HTTP-based MCP servers
10
+ class HTTPClient
11
+ def initialize(url, headers = {}, protocol_version = '2025-06-18')
12
+ @url = url
13
+ @headers = headers
14
+ @protocol_version = protocol_version
15
+ @initialized = false
16
+ @server_info = nil
17
+ end
18
+
19
+ def list_tools
20
+ ensure_initialized
21
+ response = send_request('tools/list', {})
22
+ response['tools'] || []
23
+ end
24
+
25
+ def list_prompts
26
+ ensure_initialized
27
+ response = send_request('prompts/list', {})
28
+ response['prompts'] || []
29
+ end
30
+
31
+ def list_resources
32
+ ensure_initialized
33
+ response = send_request('resources/list', {})
34
+ response['resources'] || []
35
+ end
36
+
37
+ def call_tool(tool_name, arguments)
38
+ ensure_initialized
39
+ response = send_request('tools/call', {
40
+ name: tool_name,
41
+ arguments: arguments
42
+ })
43
+ response['content'] || response
44
+ end
45
+
46
+ def get_prompt(prompt_name, arguments)
47
+ ensure_initialized
48
+ response = send_request('prompts/get', {
49
+ name: prompt_name,
50
+ arguments: arguments
51
+ })
52
+ if response['messages']
53
+ response['messages'].map { |msg| msg['content']['text'] || msg['content'] }.join("\n\n")
54
+ else
55
+ response
56
+ end
57
+ end
58
+
59
+ def get_server_info
60
+ ensure_initialized
61
+ @server_info || {}
62
+ end
63
+
64
+ private
65
+
66
+ def ensure_initialized
67
+ return if @initialized
68
+
69
+ response = send_request('initialize', {
70
+ protocolVersion: @protocol_version,
71
+ capabilities: {
72
+ roots: { listChanged: true },
73
+ sampling: {}
74
+ },
75
+ clientInfo: {
76
+ name: 'MCP CLI',
77
+ version: MCPCli::VERSION
78
+ }
79
+ })
80
+
81
+ @server_info = response['serverInfo'] if response['serverInfo']
82
+ @initialized = true
83
+ end
84
+
85
+ def send_request(method, params)
86
+ uri = URI(@url)
87
+ message = {
88
+ jsonrpc: '2.0',
89
+ id: SecureRandom.uuid,
90
+ method: method,
91
+ params: params
92
+ }
93
+
94
+ http = Net::HTTP.new(uri.host, uri.port)
95
+ http.use_ssl = uri.scheme == 'https'
96
+ http.read_timeout = 30
97
+
98
+ request = Net::HTTP::Post.new(uri.path.empty? ? '/' : uri.path)
99
+ request['Content-Type'] = 'application/json'
100
+ request['Accept'] = 'text/event-stream, application/json'
101
+ @headers.each do |key, value|
102
+ request[key] = value
103
+ end
104
+ request.body = JSON.generate(message)
105
+
106
+ response = http.request(request)
107
+ unless response.code == '200'
108
+ puts "Response headers: #{response.to_hash}" if ENV['DEBUG']
109
+ puts "Response body: #{response.body}" if ENV['DEBUG']
110
+ raise "HTTP error: #{response.code} #{response.message}"
111
+ end
112
+
113
+ parse_sse_response(response.body)
114
+ end
115
+
116
+ def parse_sse_response(body)
117
+ lines = body.split("\n")
118
+ result = nil
119
+ error = nil
120
+
121
+ lines.each do |line|
122
+ next if line.strip.empty?
123
+
124
+ next unless line.start_with?('data: ')
125
+
126
+ data = line[6..]
127
+ next if data == '[DONE]'
128
+
129
+ begin
130
+ json = JSON.parse(data)
131
+ if json['error']
132
+ error = json['error']
133
+ elsif json['result']
134
+ result = json['result']
135
+ end
136
+ rescue JSON::ParserError
137
+ puts "Failed to parse SSE data: #{data}" if ENV['DEBUG']
138
+ end
139
+ end
140
+
141
+ if error
142
+ raise "MCP Error: #{error['message'] || error.to_s}"
143
+ elsif result.nil?
144
+ raise 'No result received from server'
145
+ end
146
+
147
+ result
148
+ end
149
+
150
+ def cleanup
151
+ # Nothing to clean up for HTTP client
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCPCli
4
+ VERSION = '1.0.0'
5
+ end
data/lib/mcp_cli.rb ADDED
@@ -0,0 +1,554 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'optparse'
5
+ require_relative 'mcp_cli/version'
6
+ require_relative 'mcp_cli/client'
7
+ require_relative 'mcp_cli/http_client'
8
+
9
+ module MCPCli
10
+ # Main CLI interface for MCP servers
11
+ class CLI
12
+ FILE_NAME = 'mcp'
13
+
14
+ def initialize
15
+ @commands = {
16
+ 'list' => method(:list_servers),
17
+ 'tools' => method(:list_tools),
18
+ 'prompts' => method(:list_prompts),
19
+ 'resources' => method(:list_resources),
20
+ 'call' => method(:call_tool),
21
+ 'prompt' => method(:call_prompt),
22
+ 'info' => method(:show_info),
23
+ 'version' => method(:show_version),
24
+ 'config' => method(:show_config)
25
+ }
26
+ @claude_config_path = File.expand_path('~/.claude.json')
27
+ @cursor_config_path = File.expand_path('~/.cursor/mcp.json')
28
+ @vscode_config_path = File.expand_path('~/.vscode/mcp.json')
29
+ @mcp_config_path = nil
30
+ @protocol_version = '2025-06-18'
31
+ end
32
+
33
+ def run(args)
34
+ parsed_args = parse_args(args)
35
+
36
+ if parsed_args[:version]
37
+ show_self_version
38
+ exit 0
39
+ end
40
+
41
+ if parsed_args[:help] || parsed_args[:command].nil?
42
+ show_help
43
+ exit 0
44
+ end
45
+
46
+ @mcp_config_path = parsed_args[:mcp_config] if parsed_args[:mcp_config]
47
+ @protocol_version = parsed_args[:protocol_version]
48
+ command = parsed_args[:command]
49
+
50
+ if @commands.key?(command)
51
+ @commands[command].call(parsed_args[:args])
52
+ else
53
+ puts "Unknown command: #{command}"
54
+ show_help
55
+ exit 1
56
+ end
57
+ rescue StandardError => e
58
+ puts "Error: #{e.message}"
59
+ exit 1
60
+ end
61
+
62
+ private
63
+
64
+ def create_mcp_client(server_info)
65
+ case server_info[:type]
66
+ when 'stdio'
67
+ env = ENV.to_h.merge(server_info[:env] || {})
68
+ MCPCli::Client.new(server_info[:command], server_info[:args], env, @protocol_version)
69
+ when 'streamable-http', 'http'
70
+ MCPCli::HTTPClient.new(server_info[:url], server_info[:headers], @protocol_version)
71
+ else
72
+ raise "Unsupported server type: #{server_info[:type]}"
73
+ end
74
+ end
75
+
76
+ def parse_args(args)
77
+ result = { help: false, version: false, mcp_config: nil, protocol_version: '2025-06-18', command: nil, args: [] }
78
+ remaining_args = []
79
+
80
+ i = 0
81
+ while i < args.length
82
+ case args[i]
83
+ when '--help', '-h'
84
+ result[:help] = true
85
+ return result
86
+ when '--version', '-v'
87
+ result[:version] = true
88
+ return result
89
+ when '--mcp-config'
90
+ raise '--mcp-config requires a path argument' unless i + 1 < args.length
91
+
92
+ result[:mcp_config] = args[i + 1]
93
+ i += 1
94
+ when '--protocol-version'
95
+ raise '--protocol-version requires a version argument' unless i + 1 < args.length
96
+
97
+ result[:protocol_version] = args[i + 1]
98
+ i += 1
99
+
100
+ else
101
+ remaining_args << args[i]
102
+ end
103
+ i += 1
104
+ end
105
+
106
+ unless remaining_args.empty?
107
+ result[:command] = remaining_args[0]
108
+ result[:args] = remaining_args[1..] || []
109
+ end
110
+
111
+ result
112
+ end
113
+
114
+ def show_self_version
115
+ puts "#{MCPCli::VERSION} (MCP CLI)"
116
+ end
117
+
118
+ def show_help
119
+ puts <<~HELP
120
+ MCP CLI #{MCPCli::VERSION} - Call MCP servers from the command line
121
+
122
+ Usage:
123
+ #{FILE_NAME} list List available MCP servers
124
+ #{FILE_NAME} tools <server_name> [tool_name] List tools or show tool details
125
+ #{FILE_NAME} prompts <server_name> List prompts available on a server
126
+ #{FILE_NAME} resources <server_name> List resources available on a server
127
+ #{FILE_NAME} call <server_name> <tool> [args] Call a tool on a server (JSON or --flags)
128
+ #{FILE_NAME} prompt <server_name> <prompt> [args] Get a prompt from a server
129
+ #{FILE_NAME} info <server_name> Get detailed server information
130
+ #{FILE_NAME} version <server_name> Get server version only
131
+ #{FILE_NAME} config <server_name> Show server configuration
132
+
133
+ Options:
134
+ --mcp-config <path> Path to MCP configuration JSON file
135
+ (defaults to first found: ~/.claude.json, ~/.cursor/mcp.json, ~/.vscode/mcp.json)
136
+ (use 'claude', 'cursor', or 'vscode' to specify a default)
137
+ --protocol-version <ver> MCP protocol version (defaults to #{@protocol_version})
138
+
139
+ Examples:
140
+ #{FILE_NAME} list
141
+ #{FILE_NAME} tools vault
142
+ #{FILE_NAME} tools vault search_all
143
+ #{FILE_NAME} prompts vault
144
+ #{FILE_NAME} resources vault
145
+ #{FILE_NAME} call vault search_all '{"query": "example"}'
146
+ #{FILE_NAME} call vault search_all --query example
147
+ #{FILE_NAME} prompt vault explain '{"topic": "MCP servers"}'
148
+ #{FILE_NAME} info vault
149
+ #{FILE_NAME} version vault
150
+ #{FILE_NAME} config vault
151
+ #{FILE_NAME} --protocol-version #{@protocol_version} tools vault
152
+ #{FILE_NAME} --mcp-config /path/to/mcp.json tools vault
153
+ HELP
154
+ end
155
+
156
+ def list_servers(_args)
157
+ servers, config_path = load_mcp_servers_with_path
158
+
159
+ if servers.empty?
160
+ puts 'No MCP servers configured'
161
+ else
162
+ puts "Available MCP servers (from #{config_path}):"
163
+ servers.each do |name, config|
164
+ type = config['type'] || 'stdio'
165
+ case type
166
+ when 'stdio'
167
+ cmd_display = [config['command'], *config['args']].join(' ')
168
+ puts " #{name}: #{cmd_display}"
169
+ when 'streamable-http', 'http'
170
+ puts " #{name}: #{config['url']}"
171
+ else
172
+ puts " #{name}: (#{type})"
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ def list_tools(args)
179
+ if args.empty?
180
+ puts 'Error: Server name required'
181
+ puts "Usage: #{FILE_NAME} tools <server_name> [tool_name]"
182
+ exit 1
183
+ end
184
+
185
+ server_name = args[0]
186
+ tool_name = args[1]
187
+
188
+ servers = load_mcp_servers
189
+ unless servers.key?(server_name)
190
+ puts "Error: Server '#{server_name}' not found"
191
+ exit 1
192
+ end
193
+
194
+ server_info = parse_server_config(server_name, servers[server_name])
195
+
196
+ mcp_client = create_mcp_client(server_info)
197
+ tools = mcp_client.list_tools
198
+
199
+ if tools.empty?
200
+ puts "No tools available on server '#{server_name}'"
201
+ elsif tool_name
202
+ # Show details for specific tool
203
+ tool = tools.find { |t| t['name'] == tool_name }
204
+ if tool.nil?
205
+ puts "Error: Tool '#{tool_name}' not found on server '#{server_name}'"
206
+ exit 1
207
+ end
208
+
209
+ puts "Tool: #{tool['name']}"
210
+ puts "Server: #{server_name}"
211
+ puts "\nDescription:"
212
+ puts " #{tool['description']}" if tool['description']
213
+
214
+ if tool['inputSchema']
215
+ puts "\nInput Schema:"
216
+ puts JSON.pretty_generate(tool['inputSchema'])
217
+ end
218
+ else
219
+ # List all tools
220
+ puts "Tools available on '#{server_name}':"
221
+ tools.each do |tool|
222
+ puts "\n #{tool['name']}"
223
+ puts " Description: #{tool['description']}" if tool['description']
224
+ next unless tool['inputSchema'] && tool['inputSchema']['properties']
225
+
226
+ puts ' Parameters:'
227
+ tool['inputSchema']['properties'].each do |param, schema|
228
+ required = tool['inputSchema']['required']&.include?(param) ? ' (required)' : ''
229
+ puts " - #{param}: #{schema['type']}#{required}"
230
+ puts " #{schema['description']}" if schema['description']
231
+ end
232
+ end
233
+ end
234
+ end
235
+
236
+ def list_prompts(args)
237
+ if args.empty?
238
+ puts 'Error: Server name required'
239
+ puts "Usage: #{FILE_NAME} prompts <server_name>"
240
+ exit 1
241
+ end
242
+
243
+ server_name = args[0]
244
+ servers = load_mcp_servers
245
+ unless servers.key?(server_name)
246
+ puts "Error: Server '#{server_name}' not found"
247
+ exit 1
248
+ end
249
+
250
+ server_info = parse_server_config(server_name, servers[server_name])
251
+
252
+ mcp_client = create_mcp_client(server_info)
253
+ prompts = mcp_client.list_prompts
254
+
255
+ if prompts.empty?
256
+ puts "No prompts available on server '#{server_name}'"
257
+ else
258
+ puts "Prompts available on '#{server_name}':"
259
+ prompts.each do |prompt|
260
+ puts "\n #{prompt['name']}"
261
+ puts " Description: #{prompt['description']}" if prompt['description']
262
+ next unless prompt['arguments']
263
+
264
+ puts ' Arguments:'
265
+ prompt['arguments'].each do |arg|
266
+ required = arg['required'] ? ' (required)' : ''
267
+ puts " - #{arg['name']}#{required}"
268
+ puts " #{arg['description']}" if arg['description']
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+ def list_resources(args)
275
+ if args.empty?
276
+ puts 'Error: Server name required'
277
+ puts "Usage: #{FILE_NAME} resources <server_name>"
278
+ exit 1
279
+ end
280
+
281
+ server_name = args[0]
282
+ servers = load_mcp_servers
283
+ unless servers.key?(server_name)
284
+ puts "Error: Server '#{server_name}' not found"
285
+ exit 1
286
+ end
287
+
288
+ server_info = parse_server_config(server_name, servers[server_name])
289
+
290
+ mcp_client = create_mcp_client(server_info)
291
+ resources = mcp_client.list_resources
292
+
293
+ if resources.empty?
294
+ puts "No resources available on server '#{server_name}'"
295
+ else
296
+ puts "Resources available on '#{server_name}':"
297
+ resources.each do |resource|
298
+ puts "\n #{resource['uri']}"
299
+ puts " Name: #{resource['name']}" if resource['name']
300
+ puts " Description: #{resource['description']}" if resource['description']
301
+ puts " MIME type: #{resource['mimeType']}" if resource['mimeType']
302
+ end
303
+ end
304
+ end
305
+
306
+ def call_tool(args)
307
+ if args.length < 2
308
+ puts 'Error: Server name and tool name required'
309
+ puts "Usage: #{FILE_NAME} call <server_name> <tool> [arguments]"
310
+ puts "Arguments can be JSON: '{\"key\": \"value\"}'"
311
+ puts "Or flags: --key value --key2 value2"
312
+ exit 1
313
+ end
314
+
315
+ server_name = args[0]
316
+ tool_name = args[1]
317
+ remaining_args = args[2..]
318
+ # Parse tool arguments - either JSON or flags
319
+ tool_args = if remaining_args.empty?
320
+ {}
321
+ elsif remaining_args.length == 1 && remaining_args[0].start_with?('{')
322
+ JSON.parse(remaining_args[0])
323
+ else
324
+ parse_tool_flags(remaining_args)
325
+ end
326
+
327
+ servers = load_mcp_servers
328
+ unless servers.key?(server_name)
329
+ puts "Error: Server '#{server_name}' not found"
330
+ exit 1
331
+ end
332
+
333
+ server_info = parse_server_config(server_name, servers[server_name])
334
+
335
+ mcp_client = create_mcp_client(server_info)
336
+ result = mcp_client.call_tool(tool_name, tool_args)
337
+
338
+ puts JSON.pretty_generate(result)
339
+ end
340
+
341
+ def call_prompt(args)
342
+ if args.length < 2
343
+ puts 'Error: Server name and prompt name required'
344
+ puts "Usage: #{FILE_NAME} prompt <server_name> <prompt> [arguments_json]"
345
+ exit 1
346
+ end
347
+
348
+ server_name = args[0]
349
+ prompt_name = args[1]
350
+ prompt_args = args[2] ? JSON.parse(args[2]) : {}
351
+
352
+ servers = load_mcp_servers
353
+ unless servers.key?(server_name)
354
+ puts "Error: Server '#{server_name}' not found"
355
+ exit 1
356
+ end
357
+
358
+ server_info = parse_server_config(server_name, servers[server_name])
359
+
360
+ mcp_client = create_mcp_client(server_info)
361
+ result = mcp_client.get_prompt(prompt_name, prompt_args)
362
+
363
+ puts result
364
+ end
365
+
366
+ def show_info(args)
367
+ if args.empty?
368
+ puts 'Error: Server name required'
369
+ puts "Usage: #{FILE_NAME} info <server_name>"
370
+ exit 1
371
+ end
372
+
373
+ server_name = args[0]
374
+ servers = load_mcp_servers
375
+ unless servers.key?(server_name)
376
+ puts "Error: Server '#{server_name}' not found"
377
+ exit 1
378
+ end
379
+
380
+ server_config = parse_server_config(server_name, servers[server_name])
381
+
382
+ mcp_client = create_mcp_client(server_config)
383
+ server_info = mcp_client.get_server_info
384
+
385
+ puts "Server: #{server_name}"
386
+ puts "Name: #{server_info['name']}" if server_info['name']
387
+ puts "Version: #{server_info['version']}" if server_info['version']
388
+ puts "Description: #{server_info['description']}" if server_info['description']
389
+
390
+ puts "Protocol Version: #{server_info['protocolVersion']}" if server_info['protocolVersion']
391
+
392
+ return unless server_info['capabilities']
393
+
394
+ puts "\nCapabilities:"
395
+ server_info['capabilities'].each do |cap, value|
396
+ puts " #{cap}: #{value.inspect}"
397
+ end
398
+ end
399
+
400
+ def show_version(args)
401
+ if args.empty?
402
+ puts 'Error: Server name required'
403
+ puts "Usage: #{FILE_NAME} version <server_name>"
404
+ exit 1
405
+ end
406
+
407
+ server_name = args[0]
408
+ servers = load_mcp_servers
409
+ unless servers.key?(server_name)
410
+ puts "Error: Server '#{server_name}' not found"
411
+ exit 1
412
+ end
413
+
414
+ server_info = parse_server_config(server_name, servers[server_name])
415
+
416
+ mcp_client = create_mcp_client(server_info)
417
+ server_info = mcp_client.get_server_info
418
+
419
+ puts server_info['version'] || 'Version information not available'
420
+ end
421
+
422
+ def show_config(args)
423
+ if args.empty?
424
+ puts 'Error: Server name required'
425
+ puts "Usage: #{FILE_NAME} config <server_name>"
426
+ exit 1
427
+ end
428
+
429
+ server_name = args[0]
430
+ servers = load_mcp_servers
431
+ unless servers.key?(server_name)
432
+ puts "Error: Server '#{server_name}' not found"
433
+ exit 1
434
+ end
435
+
436
+ server_config = parse_server_config(server_name, servers[server_name])
437
+
438
+ puts "Server: #{server_name}"
439
+ puts "Type: #{server_config[:type]}"
440
+
441
+ case server_config[:type]
442
+ when 'stdio'
443
+ puts "Command: #{server_config[:command]}"
444
+ if server_config[:args] && !server_config[:args].empty?
445
+ puts 'Args:'
446
+ server_config[:args].each do |arg|
447
+ puts " - #{arg}"
448
+ end
449
+ end
450
+ if server_config[:env] && !server_config[:env].empty?
451
+ puts 'Environment:'
452
+ server_config[:env].each do |key, value|
453
+ puts " #{key}: #{value}"
454
+ end
455
+ end
456
+ when 'streamable-http', 'http'
457
+ puts "URL: #{server_config[:url]}"
458
+ end
459
+ end
460
+
461
+ def load_mcp_servers
462
+ servers, _ = load_mcp_servers_with_path
463
+ servers
464
+ end
465
+
466
+ def load_mcp_servers_with_path
467
+ if @mcp_config_path
468
+ config_path = case @mcp_config_path
469
+ when 'claude'
470
+ @claude_config_path
471
+ when 'cursor'
472
+ @cursor_config_path
473
+ when 'vscode'
474
+ @vscode_config_path
475
+ else
476
+ @mcp_config_path
477
+ end
478
+ unless File.exist?(config_path)
479
+ raise "MCP configuration file not found at #{config_path}"
480
+ end
481
+ else
482
+ config_path = [@claude_config_path, @cursor_config_path, @vscode_config_path].find do |path|
483
+ File.exist?(path)
484
+ end
485
+
486
+ unless config_path
487
+ raise "No MCP configuration file found. Tried:\n #{@claude_config_path}\n #{@cursor_config_path}\n #{@vscode_config_path}"
488
+ end
489
+ end
490
+
491
+ config = JSON.parse(File.read(config_path))
492
+ servers = config['mcpServers'] || config['servers'] || {}
493
+ [servers, config_path]
494
+ end
495
+
496
+ def parse_server_config(name, config)
497
+ type = config['type'] || 'stdio'
498
+
499
+ case type
500
+ when 'stdio', nil
501
+ {
502
+ name: name,
503
+ type: type,
504
+ command: config['command'],
505
+ args: config['args'] || [],
506
+ env: config['env'] || {}
507
+ }
508
+ when 'streamable-http', 'http'
509
+ {
510
+ name: name,
511
+ type: type,
512
+ url: config['url'],
513
+ headers: config['headers'] || {},
514
+ }
515
+ else
516
+ raise "Server '#{name}' has unsupported type: #{type}"
517
+ end
518
+ end
519
+
520
+ def parse_tool_flags(args)
521
+ result = {}
522
+ i = 0
523
+ while i < args.length
524
+ arg = args[i]
525
+ if arg.start_with?('--')
526
+ key = arg[2..] # Remove '--' prefix
527
+ # Check if there's a value following this flag
528
+ if i + 1 < args.length && !args[i + 1].start_with?('--')
529
+ # Next argument is the value
530
+ value = args[i + 1]
531
+ # Try to parse the value as JSON if it looks like JSON
532
+ result[key] = if value =~ /^(\{|\[|true|false|null|\d+(\.\d+)?$)/
533
+ begin
534
+ JSON.parse(value)
535
+ rescue JSON::ParserError
536
+ value # If parsing fails, use as string
537
+ end
538
+ else
539
+ value
540
+ end
541
+ i += 2
542
+ else
543
+ # Boolean flag without a value
544
+ result[key] = true
545
+ i += 1
546
+ end
547
+ else
548
+ raise "Invalid argument: '#{arg}'. Arguments must be in --key value format or JSON."
549
+ end
550
+ end
551
+ result
552
+ end
553
+ end
554
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mcp_cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Josh Beckman
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Command-line interface for interacting with MCP (Model Context Protocol)
13
+ servers. Supports stdio and HTTP transports.
14
+ email:
15
+ - josh@joshbeckman.org
16
+ executables:
17
+ - mcp
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - LICENSE
23
+ - README.md
24
+ - exe/mcp
25
+ - lib/mcp_cli.rb
26
+ - lib/mcp_cli/client.rb
27
+ - lib/mcp_cli/http_client.rb
28
+ - lib/mcp_cli/version.rb
29
+ homepage: https://github.com/joshbeckman/mcp_cli
30
+ licenses:
31
+ - MIT
32
+ metadata:
33
+ homepage_uri: https://github.com/joshbeckman/mcp_cli
34
+ source_code_uri: https://github.com/joshbeckman/mcp_cli
35
+ documentation_uri: https://github.com/joshbeckman/mcp_cli#readme
36
+ changelog_uri: https://github.com/joshbeckman/mcp_cli/blob/main/CHANGELOG.md
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: 2.7.0
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubygems_version: 3.7.2
52
+ specification_version: 4
53
+ summary: CLI for Model Context Protocol servers
54
+ test_files: []