mcp-inspector 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCPInspector
4
+ module Transport
5
+ class BaseAdapter
6
+ class ConnectionError < Error; end
7
+ class OperationError < Error; end
8
+ class TimeoutError < Error; end
9
+
10
+ def initialize(timeout: 30)
11
+ @timeout = timeout
12
+ @connected = false
13
+ end
14
+
15
+ def connect(server_config)
16
+ raise NotImplementedError, "Subclasses must implement #connect"
17
+ end
18
+
19
+ def disconnect
20
+ raise NotImplementedError, "Subclasses must implement #disconnect"
21
+ end
22
+
23
+ def connected?
24
+ @connected
25
+ end
26
+
27
+ def list_tools
28
+ ensure_connected!
29
+ raise NotImplementedError, "Subclasses must implement #list_tools"
30
+ end
31
+
32
+ def list_resources
33
+ ensure_connected!
34
+ raise NotImplementedError, "Subclasses must implement #list_resources"
35
+ end
36
+
37
+ def list_prompts
38
+ ensure_connected!
39
+ raise NotImplementedError, "Subclasses must implement #list_prompts"
40
+ end
41
+
42
+ def execute_tool(name, arguments = {})
43
+ ensure_connected!
44
+ raise NotImplementedError, "Subclasses must implement #execute_tool"
45
+ end
46
+
47
+ def read_resource(uri)
48
+ ensure_connected!
49
+ raise NotImplementedError, "Subclasses must implement #read_resource"
50
+ end
51
+
52
+ def get_prompt(name, arguments = {})
53
+ ensure_connected!
54
+ raise NotImplementedError, "Subclasses must implement #get_prompt"
55
+ end
56
+
57
+ def server_info
58
+ ensure_connected!
59
+ raise NotImplementedError, "Subclasses must implement #server_info"
60
+ end
61
+
62
+ private
63
+
64
+ attr_reader :timeout
65
+
66
+ def ensure_connected!
67
+ raise ConnectionError, "Not connected to server" unless connected?
68
+ end
69
+
70
+ def with_timeout
71
+ if timeout
72
+ Timeout.timeout(timeout) { yield }
73
+ else
74
+ yield
75
+ end
76
+ rescue Timeout::Error
77
+ raise TimeoutError, "Operation timed out after #{timeout} seconds"
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+ require "mcp_client"
3
+
4
+ module MCPInspector
5
+ module Transport
6
+ class ClientAdapter < BaseAdapter
7
+ def initialize(timeout: 30)
8
+ super
9
+ @client = nil
10
+ @server_config = nil
11
+ end
12
+
13
+ def connect(server_config)
14
+ @server_config = server_config
15
+
16
+ begin
17
+ with_timeout do
18
+ case server_config.transport
19
+ when "stdio"
20
+ connect_stdio
21
+ when "sse"
22
+ connect_sse
23
+ when "websocket"
24
+ connect_websocket
25
+ else
26
+ raise ConnectionError, "Unsupported transport: #{server_config.transport}"
27
+ end
28
+ end
29
+ @connected = true
30
+ rescue => e
31
+ @connected = false
32
+ raise ConnectionError, "Failed to connect to #{server_config.name}: #{e.message}"
33
+ end
34
+ end
35
+
36
+ def disconnect
37
+ if @client && @client.respond_to?(:disconnect)
38
+ @client.disconnect
39
+ end
40
+ @client = nil
41
+ @connected = false
42
+ end
43
+
44
+ def list_tools
45
+ with_timeout do
46
+ response = @client.list_tools
47
+ normalize_response(response)
48
+ end
49
+ rescue => e
50
+ raise OperationError, "Failed to list tools: #{e.message}"
51
+ end
52
+
53
+ def list_resources
54
+ if @client.respond_to?(:list_resources)
55
+ with_timeout do
56
+ response = @client.list_resources
57
+ normalize_response(response)
58
+ end
59
+ else
60
+ []
61
+ end
62
+ rescue => e
63
+ raise OperationError, "Failed to list resources: #{e.message}"
64
+ end
65
+
66
+ def list_prompts
67
+ if @client.respond_to?(:list_prompts)
68
+ with_timeout do
69
+ response = @client.list_prompts
70
+ normalize_response(response)
71
+ end
72
+ else
73
+ # Return empty prompts list if not supported
74
+ []
75
+ end
76
+ rescue => e
77
+ raise OperationError, "Failed to list prompts: #{e.message}"
78
+ end
79
+
80
+ def execute_tool(name, arguments = {})
81
+ with_timeout do
82
+ response = @client.call_tool(name, arguments)
83
+ normalize_response(response)
84
+ end
85
+ rescue => e
86
+ raise OperationError, "Failed to execute tool '#{name}': #{e.message}"
87
+ end
88
+
89
+ def read_resource(uri)
90
+ if @client.respond_to?(:read_resource)
91
+ with_timeout do
92
+ response = @client.read_resource(uri)
93
+ normalize_response(response)
94
+ end
95
+ else
96
+ raise OperationError, "Resource reading is not supported by this server"
97
+ end
98
+ rescue => e
99
+ raise OperationError, "Failed to read resource '#{uri}': #{e.message}"
100
+ end
101
+
102
+ def get_prompt(name, arguments = {})
103
+ if @client.respond_to?(:get_prompt)
104
+ with_timeout do
105
+ response = @client.get_prompt(name, arguments)
106
+ normalize_response(response)
107
+ end
108
+ else
109
+ raise OperationError, "Prompts are not supported by this server"
110
+ end
111
+ rescue => e
112
+ raise OperationError, "Failed to get prompt '#{name}': #{e.message}"
113
+ end
114
+
115
+ def server_info
116
+ {
117
+ name: @server_config.name,
118
+ transport: @server_config.transport,
119
+ connected: connected?,
120
+ capabilities: detect_capabilities
121
+ }
122
+ end
123
+
124
+ private
125
+
126
+ def connect_stdio
127
+ @client = MCPClient::ServerStdio.new(
128
+ command: @server_config.command.join(' '),
129
+ env: @server_config.env
130
+ )
131
+ @client.connect
132
+ end
133
+
134
+ def connect_sse
135
+ @client = MCPClient::ServerSSE.new(url: @server_config.url)
136
+ @client.connect
137
+ end
138
+
139
+ def connect_websocket
140
+ @client = MCPClient::ServerHTTP.new(url: @server_config.url)
141
+ @client.connect
142
+ end
143
+
144
+ def normalize_response(response)
145
+ case response
146
+ when Hash
147
+ response
148
+ when Array
149
+ # Handle arrays of MCPClient objects
150
+ if response.first.respond_to?(:name) && response.first.respond_to?(:description)
151
+ # Array of Tool, Resource, or Prompt objects
152
+ response.map { |item| normalize_mcp_object(item) }
153
+ else
154
+ response
155
+ end
156
+ else
157
+ response
158
+ end
159
+ end
160
+
161
+ def normalize_mcp_object(obj)
162
+ base_data = {
163
+ name: obj.name,
164
+ description: obj.description
165
+ }
166
+
167
+ # Add schema for tools
168
+ if obj.respond_to?(:schema) && obj.schema
169
+ base_data[:inputSchema] = obj.schema
170
+ end
171
+
172
+ # Add other properties that might exist
173
+ base_data[:uri] = obj.uri if obj.respond_to?(:uri)
174
+ base_data[:mimeType] = obj.mimeType if obj.respond_to?(:mimeType)
175
+
176
+ base_data
177
+ end
178
+
179
+ def detect_capabilities
180
+ capabilities = []
181
+
182
+ begin
183
+ list_tools
184
+ capabilities << "tools"
185
+ rescue OperationError
186
+ # Tools not supported
187
+ end
188
+
189
+ begin
190
+ list_resources
191
+ capabilities << "resources"
192
+ rescue OperationError
193
+ # Resources not supported
194
+ end
195
+
196
+ begin
197
+ list_prompts
198
+ capabilities << "prompts"
199
+ rescue OperationError
200
+ # Prompts not supported
201
+ end
202
+
203
+ capabilities
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module MCPInspector
6
+ module Transport
7
+ class ServerConfig
8
+ class ValidationError < Error; end
9
+
10
+ REQUIRED_FIELDS = %w[name transport].freeze
11
+ STDIO_REQUIRED_FIELDS = %w[command].freeze
12
+ URL_REQUIRED_FIELDS = %w[url].freeze
13
+ VALID_TRANSPORTS = %w[stdio sse websocket].freeze
14
+
15
+ attr_reader :name, :transport, :command, :args, :env, :working_directory, :url
16
+
17
+ def initialize(config_hash)
18
+ @raw_config = config_hash
19
+ validate_and_parse_config!
20
+ end
21
+
22
+ def self.from_json(json_string)
23
+ config_hash = JSON.parse(json_string)
24
+ new(config_hash)
25
+ rescue JSON::ParserError => e
26
+ raise ValidationError, "Invalid JSON: #{e.message}"
27
+ end
28
+
29
+ def stdio?
30
+ transport == "stdio"
31
+ end
32
+
33
+ def sse?
34
+ transport == "sse"
35
+ end
36
+
37
+ def websocket?
38
+ transport == "websocket"
39
+ end
40
+
41
+ def to_h
42
+ {
43
+ name: name,
44
+ transport: transport,
45
+ command: command,
46
+ args: args,
47
+ env: env,
48
+ working_directory: working_directory,
49
+ url: url
50
+ }.compact
51
+ end
52
+
53
+ def to_json(*args)
54
+ to_h.to_json(*args)
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :raw_config
60
+
61
+ def validate_and_parse_config!
62
+ validate_required_fields!
63
+ validate_transport!
64
+ validate_transport_specific_fields!
65
+ parse_fields!
66
+ end
67
+
68
+ def validate_required_fields!
69
+ missing_fields = REQUIRED_FIELDS - raw_config.keys
70
+ return if missing_fields.empty?
71
+
72
+ raise ValidationError, "Missing required fields: #{missing_fields.join(', ')}"
73
+ end
74
+
75
+ def validate_transport!
76
+ unless VALID_TRANSPORTS.include?(raw_config["transport"])
77
+ raise ValidationError, "Invalid transport '#{raw_config['transport']}'. Valid options: #{VALID_TRANSPORTS.join(', ')}"
78
+ end
79
+ end
80
+
81
+ def validate_transport_specific_fields!
82
+ case raw_config["transport"]
83
+ when "stdio"
84
+ validate_stdio_fields!
85
+ when "sse", "websocket"
86
+ validate_url_fields!
87
+ end
88
+ end
89
+
90
+ def validate_stdio_fields!
91
+ missing_fields = STDIO_REQUIRED_FIELDS - raw_config.keys
92
+ return if missing_fields.empty?
93
+
94
+ raise ValidationError, "Missing required fields for stdio transport: #{missing_fields.join(', ')}"
95
+ end
96
+
97
+ def validate_url_fields!
98
+ missing_fields = URL_REQUIRED_FIELDS - raw_config.keys
99
+ return if missing_fields.empty?
100
+
101
+ raise ValidationError, "Missing required fields for URL-based transport: #{missing_fields.join(', ')}"
102
+ end
103
+
104
+ def parse_fields!
105
+ @name = raw_config["name"]
106
+ @transport = raw_config["transport"]
107
+ @command = parse_command(raw_config["command"]) if raw_config["command"]
108
+ @args = raw_config["args"] || []
109
+ @env = parse_env(raw_config["env"] || {})
110
+ @working_directory = raw_config["working_directory"] || "."
111
+ @url = raw_config["url"] if raw_config["url"]
112
+ end
113
+
114
+ def parse_command(command)
115
+ case command
116
+ when Array
117
+ command
118
+ when String
119
+ command.split
120
+ else
121
+ raise ValidationError, "Command must be a string or array of strings"
122
+ end
123
+ end
124
+
125
+ def parse_env(env_hash)
126
+ return {} unless env_hash.is_a?(Hash)
127
+
128
+ env_hash.transform_values { |value| expand_env_variable(value) }
129
+ end
130
+
131
+ def expand_env_variable(value)
132
+ return value unless value.is_a?(String)
133
+
134
+ value.gsub(/\$\{([^}]+)\}/) do |match|
135
+ env_var = ::Regexp.last_match(1)
136
+ ENV[env_var] || match
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCPInspector
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+ require "timeout"
5
+
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.setup
8
+
9
+ module MCPInspector
10
+ class Error < StandardError; end
11
+ end
12
+
13
+ # Eagerly load CLI so it's available for the executable
14
+ require_relative "mcp_inspector/cli"
@@ -0,0 +1,6 @@
1
+ module Mcp
2
+ module Inspector
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,174 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mcp-inspector
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Enrique Mogollan
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-08-26 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: thor
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.3'
26
+ - !ruby/object:Gem::Dependency
27
+ name: json
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.6'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.6'
40
+ - !ruby/object:Gem::Dependency
41
+ name: zeitwerk
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.6'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.6'
54
+ - !ruby/object:Gem::Dependency
55
+ name: ruby-mcp-client
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 0.7.0
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: 0.7.0
68
+ - !ruby/object:Gem::Dependency
69
+ name: base64
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: bundler
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rake
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '13.0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '13.0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rspec
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '3.12'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '3.12'
124
+ description: A Ruby gem that provides tooling for connecting to and inspecting MCP
125
+ servers, allowing you to list and execute tools, resources, and prompts with JSON
126
+ output.
127
+ email:
128
+ - emogollan@gmail.com
129
+ executables:
130
+ - mcp-inspector
131
+ extensions: []
132
+ extra_rdoc_files: []
133
+ files:
134
+ - ".rspec"
135
+ - README.md
136
+ - Rakefile
137
+ - examples/mcp-inspector.json
138
+ - exe/mcp-inspector
139
+ - lib/mcp_inspector.rb
140
+ - lib/mcp_inspector/cli.rb
141
+ - lib/mcp_inspector/data/config_manager.rb
142
+ - lib/mcp_inspector/data/input_adapter.rb
143
+ - lib/mcp_inspector/data/output_adapter.rb
144
+ - lib/mcp_inspector/presentation/base_formatter.rb
145
+ - lib/mcp_inspector/presentation/json_formatter.rb
146
+ - lib/mcp_inspector/transport/base_adapter.rb
147
+ - lib/mcp_inspector/transport/client_adapter.rb
148
+ - lib/mcp_inspector/transport/server_config.rb
149
+ - lib/mcp_inspector/version.rb
150
+ - sig/mcp/inspector.rbs
151
+ homepage: https://github.com/mogox/mcp-inspector
152
+ licenses: []
153
+ metadata:
154
+ homepage_uri: https://github.com/mogox/mcp-inspector
155
+ source_code_uri: https://github.com/mogox/mcp-inspector
156
+ bug_tracker_uri: https://github.com/mogox/mcp-inspector/issues
157
+ rdoc_options: []
158
+ require_paths:
159
+ - lib
160
+ required_ruby_version: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: 2.7.0
165
+ required_rubygems_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ requirements: []
171
+ rubygems_version: 3.6.6
172
+ specification_version: 4
173
+ summary: A tool for inspecting MCP (Model Context Protocol) servers
174
+ test_files: []