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 +7 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE +21 -0
- data/README.md +217 -0
- data/exe/mcp +6 -0
- data/lib/mcp_cli/client.rb +157 -0
- data/lib/mcp_cli/http_client.rb +154 -0
- data/lib/mcp_cli/version.rb +5 -0
- data/lib/mcp_cli.rb +554 -0
- metadata +54 -0
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,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
|
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: []
|