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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/README.md +261 -0
- data/Rakefile +8 -0
- data/examples/mcp-inspector.json +28 -0
- data/exe/mcp-inspector +14 -0
- data/lib/mcp_inspector/cli.rb +257 -0
- data/lib/mcp_inspector/data/config_manager.rb +238 -0
- data/lib/mcp_inspector/data/input_adapter.rb +69 -0
- data/lib/mcp_inspector/data/output_adapter.rb +110 -0
- data/lib/mcp_inspector/presentation/base_formatter.rb +93 -0
- data/lib/mcp_inspector/presentation/json_formatter.rb +153 -0
- data/lib/mcp_inspector/transport/base_adapter.rb +81 -0
- data/lib/mcp_inspector/transport/client_adapter.rb +207 -0
- data/lib/mcp_inspector/transport/server_config.rb +141 -0
- data/lib/mcp_inspector/version.rb +5 -0
- data/lib/mcp_inspector.rb +14 -0
- data/sig/mcp/inspector.rbs +6 -0
- metadata +174 -0
@@ -0,0 +1,238 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "pathname"
|
5
|
+
require "fileutils"
|
6
|
+
|
7
|
+
module MCPInspector
|
8
|
+
module Data
|
9
|
+
class ConfigManager
|
10
|
+
class ConfigError < Error; end
|
11
|
+
|
12
|
+
DEFAULT_USER_CONFIG_PATH = File.expand_path("~/.mcp-inspector.json")
|
13
|
+
DEFAULT_PROJECT_CONFIG_PATH = "./.mcp-inspector.json"
|
14
|
+
|
15
|
+
def initialize(config_path: nil)
|
16
|
+
@config_path = config_path
|
17
|
+
@config = load_merged_config
|
18
|
+
validate_config!
|
19
|
+
end
|
20
|
+
|
21
|
+
def servers
|
22
|
+
@servers ||= build_server_configs
|
23
|
+
end
|
24
|
+
|
25
|
+
def server_names
|
26
|
+
servers.keys
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_server(name)
|
30
|
+
servers[name] or raise ConfigError, "Server '#{name}' not found"
|
31
|
+
end
|
32
|
+
|
33
|
+
def defaults
|
34
|
+
@config["defaults"] || {}
|
35
|
+
end
|
36
|
+
|
37
|
+
def output_format
|
38
|
+
defaults["output"] || "json"
|
39
|
+
end
|
40
|
+
|
41
|
+
def pretty_print?
|
42
|
+
defaults.fetch("pretty", true)
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_h
|
46
|
+
@config
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.config_file_exists?(path = nil)
|
50
|
+
paths_to_check = if path
|
51
|
+
[path]
|
52
|
+
else
|
53
|
+
[DEFAULT_PROJECT_CONFIG_PATH, DEFAULT_USER_CONFIG_PATH]
|
54
|
+
end
|
55
|
+
|
56
|
+
paths_to_check.any? { |p| File.exist?(p) }
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.create_example_config(path = DEFAULT_USER_CONFIG_PATH)
|
60
|
+
example_config = {
|
61
|
+
"servers" => [
|
62
|
+
{
|
63
|
+
"name" => "filesystem-server",
|
64
|
+
"transport" => "stdio",
|
65
|
+
"command" => ["npx", "-y", "@modelcontextprotocol/server-filesystem"],
|
66
|
+
"args" => ["/tmp"],
|
67
|
+
"env" => {}
|
68
|
+
},
|
69
|
+
{
|
70
|
+
"name" => "github-server",
|
71
|
+
"transport" => "stdio",
|
72
|
+
"command" => ["npx", "-y", "@modelcontextprotocol/server-github"],
|
73
|
+
"env" => {
|
74
|
+
"GITHUB_TOKEN" => "${GITHUB_TOKEN}"
|
75
|
+
}
|
76
|
+
}
|
77
|
+
],
|
78
|
+
"defaults" => {
|
79
|
+
"output" => "json",
|
80
|
+
"pretty" => true
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
File.write(path, JSON.pretty_generate(example_config))
|
85
|
+
path
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
attr_reader :config_path
|
91
|
+
|
92
|
+
def load_merged_config
|
93
|
+
configs = []
|
94
|
+
|
95
|
+
# Load in order of precedence (last wins)
|
96
|
+
configs << load_user_config if user_config_exists?
|
97
|
+
configs << load_project_config if project_config_exists?
|
98
|
+
configs << load_custom_config if custom_config_specified?
|
99
|
+
|
100
|
+
if configs.empty?
|
101
|
+
# Auto-create default config file like Claude Desktop does
|
102
|
+
created_config_path = auto_create_default_config
|
103
|
+
raise ConfigError, build_no_config_error_message(created_config_path)
|
104
|
+
end
|
105
|
+
|
106
|
+
merge_configs(configs)
|
107
|
+
end
|
108
|
+
|
109
|
+
def load_user_config
|
110
|
+
load_config_file(DEFAULT_USER_CONFIG_PATH)
|
111
|
+
end
|
112
|
+
|
113
|
+
def load_project_config
|
114
|
+
load_config_file(DEFAULT_PROJECT_CONFIG_PATH)
|
115
|
+
end
|
116
|
+
|
117
|
+
def load_custom_config
|
118
|
+
load_config_file(config_path)
|
119
|
+
end
|
120
|
+
|
121
|
+
def load_config_file(path)
|
122
|
+
content = File.read(path)
|
123
|
+
JSON.parse(content)
|
124
|
+
rescue JSON::ParserError => e
|
125
|
+
raise ConfigError, "Invalid JSON in config file '#{path}': #{e.message}"
|
126
|
+
rescue Errno::ENOENT
|
127
|
+
raise ConfigError, "Configuration file not found: #{path}"
|
128
|
+
rescue Errno::EACCES
|
129
|
+
raise ConfigError, "Configuration file not readable: #{path}"
|
130
|
+
end
|
131
|
+
|
132
|
+
def merge_configs(configs)
|
133
|
+
base_config = { "servers" => [], "defaults" => {} }
|
134
|
+
|
135
|
+
configs.each do |config|
|
136
|
+
base_config = deep_merge(base_config, config)
|
137
|
+
end
|
138
|
+
|
139
|
+
base_config
|
140
|
+
end
|
141
|
+
|
142
|
+
def deep_merge(hash1, hash2)
|
143
|
+
result = hash1.dup
|
144
|
+
|
145
|
+
hash2.each do |key, value|
|
146
|
+
if result[key].is_a?(Hash) && value.is_a?(Hash)
|
147
|
+
result[key] = deep_merge(result[key], value)
|
148
|
+
elsif key == "servers" && result[key].is_a?(Array) && value.is_a?(Array)
|
149
|
+
result[key] = merge_servers(result[key], value)
|
150
|
+
else
|
151
|
+
result[key] = value
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
result
|
156
|
+
end
|
157
|
+
|
158
|
+
def merge_servers(servers1, servers2)
|
159
|
+
merged = servers1.dup
|
160
|
+
servers2.each do |server|
|
161
|
+
existing_index = merged.find_index { |s| s["name"] == server["name"] }
|
162
|
+
if existing_index
|
163
|
+
merged[existing_index] = server
|
164
|
+
else
|
165
|
+
merged << server
|
166
|
+
end
|
167
|
+
end
|
168
|
+
merged
|
169
|
+
end
|
170
|
+
|
171
|
+
def validate_config!
|
172
|
+
raise ConfigError, "Configuration must be a hash" unless @config.is_a?(Hash)
|
173
|
+
raise ConfigError, "No 'servers' section found in configuration" unless @config["servers"]
|
174
|
+
raise ConfigError, "'servers' must be an array" unless @config["servers"].is_a?(Array)
|
175
|
+
raise ConfigError, "No servers configured" if @config["servers"].empty?
|
176
|
+
end
|
177
|
+
|
178
|
+
def build_server_configs
|
179
|
+
server_configs = {}
|
180
|
+
|
181
|
+
@config["servers"].each do |server_hash|
|
182
|
+
begin
|
183
|
+
server_config = MCPInspector::Transport::ServerConfig.new(server_hash)
|
184
|
+
server_configs[server_config.name] = server_config
|
185
|
+
rescue MCPInspector::Transport::ServerConfig::ValidationError => e
|
186
|
+
raise ConfigError, "Invalid server configuration: #{e.message}"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
server_configs
|
191
|
+
end
|
192
|
+
|
193
|
+
def user_config_exists?
|
194
|
+
File.exist?(DEFAULT_USER_CONFIG_PATH)
|
195
|
+
end
|
196
|
+
|
197
|
+
def project_config_exists?
|
198
|
+
File.exist?(DEFAULT_PROJECT_CONFIG_PATH)
|
199
|
+
end
|
200
|
+
|
201
|
+
def custom_config_specified?
|
202
|
+
config_path && File.exist?(config_path)
|
203
|
+
end
|
204
|
+
|
205
|
+
def auto_create_default_config
|
206
|
+
# Determine where to create the config file
|
207
|
+
config_file_path = config_path || DEFAULT_USER_CONFIG_PATH
|
208
|
+
|
209
|
+
# Ensure the parent directory exists
|
210
|
+
parent_dir = File.dirname(config_file_path)
|
211
|
+
FileUtils.mkdir_p(parent_dir) unless File.directory?(parent_dir)
|
212
|
+
|
213
|
+
# Create the config file with example servers
|
214
|
+
self.class.create_example_config(config_file_path)
|
215
|
+
|
216
|
+
config_file_path
|
217
|
+
end
|
218
|
+
|
219
|
+
def build_no_config_error_message(created_config_path)
|
220
|
+
<<~ERROR
|
221
|
+
No configuration file found, so I created one for you at:
|
222
|
+
#{created_config_path}
|
223
|
+
|
224
|
+
This file contains example MCP server configurations. Please edit it to:
|
225
|
+
1. Add your actual MCP servers
|
226
|
+
2. Remove or modify the example servers as needed
|
227
|
+
3. Set any custom defaults
|
228
|
+
|
229
|
+
Then run your command again.
|
230
|
+
|
231
|
+
Example servers included:
|
232
|
+
- filesystem-server: For file system operations
|
233
|
+
- github-server: For GitHub operations (requires GITHUB_TOKEN)
|
234
|
+
ERROR
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module MCPInspector
|
6
|
+
module Data
|
7
|
+
class InputAdapter
|
8
|
+
class ValidationError < Error; end
|
9
|
+
|
10
|
+
def initialize(args = [], options = {})
|
11
|
+
@args = args
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse_json_arguments(json_string)
|
16
|
+
return {} if json_string.nil? || json_string.empty?
|
17
|
+
|
18
|
+
JSON.parse(json_string)
|
19
|
+
rescue JSON::ParserError => e
|
20
|
+
raise ValidationError, "Invalid JSON arguments: #{e.message}\nExpected format: '{\"key\": \"value\"}'"
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate_server_name!(server_name, available_servers)
|
24
|
+
return if available_servers.include?(server_name)
|
25
|
+
|
26
|
+
if available_servers.empty?
|
27
|
+
raise ValidationError, "No servers configured. Please add servers to your configuration file."
|
28
|
+
else
|
29
|
+
raise ValidationError, "Server '#{server_name}' not found. Available servers: #{available_servers.join(', ')}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_tool_name!(tool_name)
|
34
|
+
raise ValidationError, "Tool name is required" if tool_name.nil? || tool_name.empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_resource_uri!(uri)
|
38
|
+
raise ValidationError, "Resource URI is required" if uri.nil? || uri.empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate_prompt_name!(prompt_name)
|
42
|
+
raise ValidationError, "Prompt name is required" if prompt_name.nil? || prompt_name.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate_config_file!(config_path)
|
46
|
+
unless File.exist?(config_path)
|
47
|
+
raise ValidationError, "Configuration file not found: #{config_path}"
|
48
|
+
end
|
49
|
+
|
50
|
+
unless File.readable?(config_path)
|
51
|
+
raise ValidationError, "Configuration file is not readable: #{config_path}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def transform_to_operation_params(command, subcommand = nil, *args)
|
56
|
+
{
|
57
|
+
command: command,
|
58
|
+
subcommand: subcommand,
|
59
|
+
target: args.first,
|
60
|
+
arguments: parse_json_arguments(@options[:args])
|
61
|
+
}.compact
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
attr_reader :args, :options
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MCPInspector
|
4
|
+
module Data
|
5
|
+
class OutputAdapter
|
6
|
+
DEFAULT_FORMAT = "json"
|
7
|
+
VALID_FORMATS = %w[json terminal].freeze
|
8
|
+
|
9
|
+
def initialize(format: DEFAULT_FORMAT, pretty: true, output_destination: $stdout)
|
10
|
+
@format = validate_format(format)
|
11
|
+
@pretty = pretty
|
12
|
+
@output_destination = output_destination
|
13
|
+
@formatter = create_formatter
|
14
|
+
end
|
15
|
+
|
16
|
+
def output_success(data, metadata = {})
|
17
|
+
formatted_output = @formatter.format_success(data, metadata)
|
18
|
+
write_output(formatted_output)
|
19
|
+
end
|
20
|
+
|
21
|
+
def output_error(error, metadata = {})
|
22
|
+
formatted_output = @formatter.format_error(error, metadata)
|
23
|
+
write_output(formatted_output)
|
24
|
+
end
|
25
|
+
|
26
|
+
def output_tools_list(tools, metadata = {})
|
27
|
+
formatted_output = @formatter.format_tools_list(tools, metadata)
|
28
|
+
write_output(formatted_output)
|
29
|
+
end
|
30
|
+
|
31
|
+
def output_resources_list(resources, metadata = {})
|
32
|
+
formatted_output = @formatter.format_resources_list(resources, metadata)
|
33
|
+
write_output(formatted_output)
|
34
|
+
end
|
35
|
+
|
36
|
+
def output_prompts_list(prompts, metadata = {})
|
37
|
+
formatted_output = @formatter.format_prompts_list(prompts, metadata)
|
38
|
+
write_output(formatted_output)
|
39
|
+
end
|
40
|
+
|
41
|
+
def output_tool_result(result, metadata = {})
|
42
|
+
formatted_output = @formatter.format_tool_result(result, metadata)
|
43
|
+
write_output(formatted_output)
|
44
|
+
end
|
45
|
+
|
46
|
+
def output_resource_content(content, metadata = {})
|
47
|
+
formatted_output = @formatter.format_resource_content(content, metadata)
|
48
|
+
write_output(formatted_output)
|
49
|
+
end
|
50
|
+
|
51
|
+
def output_prompt_result(result, metadata = {})
|
52
|
+
formatted_output = @formatter.format_prompt_result(result, metadata)
|
53
|
+
write_output(formatted_output)
|
54
|
+
end
|
55
|
+
|
56
|
+
def output_server_info(info, metadata = {})
|
57
|
+
formatted_output = @formatter.format_server_info(info, metadata)
|
58
|
+
write_output(formatted_output)
|
59
|
+
end
|
60
|
+
|
61
|
+
def output_config_list(servers, metadata = {})
|
62
|
+
formatted_output = @formatter.format_config_list(servers, metadata)
|
63
|
+
write_output(formatted_output)
|
64
|
+
end
|
65
|
+
|
66
|
+
def output_config_details(server_config, metadata = {})
|
67
|
+
formatted_output = @formatter.format_config_details(server_config, metadata)
|
68
|
+
write_output(formatted_output)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
attr_reader :format, :pretty, :output_destination, :formatter
|
74
|
+
|
75
|
+
def validate_format(format)
|
76
|
+
unless VALID_FORMATS.include?(format)
|
77
|
+
raise ArgumentError, "Invalid output format '#{format}'. Valid options: #{VALID_FORMATS.join(', ')}"
|
78
|
+
end
|
79
|
+
format
|
80
|
+
end
|
81
|
+
|
82
|
+
def create_formatter
|
83
|
+
case format
|
84
|
+
when "json"
|
85
|
+
MCPInspector::Presentation::JSONFormatter.new(pretty: pretty)
|
86
|
+
when "terminal"
|
87
|
+
# Placeholder for future terminal formatter
|
88
|
+
MCPInspector::Presentation::JSONFormatter.new(pretty: pretty)
|
89
|
+
else
|
90
|
+
raise ArgumentError, "Unsupported formatter: #{format}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def write_output(formatted_output)
|
95
|
+
output_destination.puts(formatted_output)
|
96
|
+
output_destination.flush if output_destination.respond_to?(:flush)
|
97
|
+
end
|
98
|
+
|
99
|
+
def build_metadata(operation:, server: nil, **additional)
|
100
|
+
base_metadata = {
|
101
|
+
operation: operation,
|
102
|
+
timestamp: Time.now.iso8601
|
103
|
+
}
|
104
|
+
|
105
|
+
base_metadata[:server] = server if server
|
106
|
+
base_metadata.merge(additional)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MCPInspector
|
4
|
+
module Presentation
|
5
|
+
class BaseFormatter
|
6
|
+
def initialize(pretty: true)
|
7
|
+
@pretty = pretty
|
8
|
+
end
|
9
|
+
|
10
|
+
def format_success(data, metadata = {})
|
11
|
+
raise NotImplementedError, "Subclasses must implement #format_success"
|
12
|
+
end
|
13
|
+
|
14
|
+
def format_error(error, metadata = {})
|
15
|
+
raise NotImplementedError, "Subclasses must implement #format_error"
|
16
|
+
end
|
17
|
+
|
18
|
+
def format_tools_list(tools, metadata = {})
|
19
|
+
raise NotImplementedError, "Subclasses must implement #format_tools_list"
|
20
|
+
end
|
21
|
+
|
22
|
+
def format_resources_list(resources, metadata = {})
|
23
|
+
raise NotImplementedError, "Subclasses must implement #format_resources_list"
|
24
|
+
end
|
25
|
+
|
26
|
+
def format_prompts_list(prompts, metadata = {})
|
27
|
+
raise NotImplementedError, "Subclasses must implement #format_prompts_list"
|
28
|
+
end
|
29
|
+
|
30
|
+
def format_tool_result(result, metadata = {})
|
31
|
+
raise NotImplementedError, "Subclasses must implement #format_tool_result"
|
32
|
+
end
|
33
|
+
|
34
|
+
def format_resource_content(content, metadata = {})
|
35
|
+
raise NotImplementedError, "Subclasses must implement #format_resource_content"
|
36
|
+
end
|
37
|
+
|
38
|
+
def format_prompt_result(result, metadata = {})
|
39
|
+
raise NotImplementedError, "Subclasses must implement #format_prompt_result"
|
40
|
+
end
|
41
|
+
|
42
|
+
def format_server_info(info, metadata = {})
|
43
|
+
raise NotImplementedError, "Subclasses must implement #format_server_info"
|
44
|
+
end
|
45
|
+
|
46
|
+
def format_config_list(servers, metadata = {})
|
47
|
+
raise NotImplementedError, "Subclasses must implement #format_config_list"
|
48
|
+
end
|
49
|
+
|
50
|
+
def format_config_details(server_config, metadata = {})
|
51
|
+
raise NotImplementedError, "Subclasses must implement #format_config_details"
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
attr_reader :pretty
|
57
|
+
|
58
|
+
def build_response(status:, data: nil, error: nil, metadata: {})
|
59
|
+
response = {
|
60
|
+
status: status,
|
61
|
+
metadata: build_metadata(metadata)
|
62
|
+
}
|
63
|
+
|
64
|
+
response[:data] = data if data
|
65
|
+
response[:error] = format_error_message(error) if error
|
66
|
+
|
67
|
+
response
|
68
|
+
end
|
69
|
+
|
70
|
+
def build_metadata(metadata)
|
71
|
+
base_metadata = {
|
72
|
+
timestamp: Time.now.iso8601
|
73
|
+
}
|
74
|
+
|
75
|
+
base_metadata.merge(metadata.compact)
|
76
|
+
end
|
77
|
+
|
78
|
+
def format_error_message(error)
|
79
|
+
case error
|
80
|
+
when String
|
81
|
+
error
|
82
|
+
when StandardError
|
83
|
+
{
|
84
|
+
type: error.class.name,
|
85
|
+
message: error.message
|
86
|
+
}
|
87
|
+
else
|
88
|
+
error.to_s
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module MCPInspector
|
6
|
+
module Presentation
|
7
|
+
class JSONFormatter < BaseFormatter
|
8
|
+
def format_success(data, metadata = {})
|
9
|
+
response = build_response(status: "success", data: data, metadata: metadata)
|
10
|
+
to_json(response)
|
11
|
+
end
|
12
|
+
|
13
|
+
def format_error(error, metadata = {})
|
14
|
+
response = build_response(status: "error", error: error, metadata: metadata)
|
15
|
+
to_json(response)
|
16
|
+
end
|
17
|
+
|
18
|
+
def format_tools_list(tools, metadata = {})
|
19
|
+
data = {
|
20
|
+
tools: normalize_list_data(tools),
|
21
|
+
count: Array(tools).length
|
22
|
+
}
|
23
|
+
|
24
|
+
metadata = metadata.merge(operation: "list_tools")
|
25
|
+
format_success(data, metadata)
|
26
|
+
end
|
27
|
+
|
28
|
+
def format_resources_list(resources, metadata = {})
|
29
|
+
data = {
|
30
|
+
resources: normalize_list_data(resources),
|
31
|
+
count: Array(resources).length
|
32
|
+
}
|
33
|
+
|
34
|
+
metadata = metadata.merge(operation: "list_resources")
|
35
|
+
format_success(data, metadata)
|
36
|
+
end
|
37
|
+
|
38
|
+
def format_prompts_list(prompts, metadata = {})
|
39
|
+
data = {
|
40
|
+
prompts: normalize_list_data(prompts),
|
41
|
+
count: Array(prompts).length
|
42
|
+
}
|
43
|
+
|
44
|
+
metadata = metadata.merge(operation: "list_prompts")
|
45
|
+
format_success(data, metadata)
|
46
|
+
end
|
47
|
+
|
48
|
+
def format_tool_result(result, metadata = {})
|
49
|
+
data = {
|
50
|
+
result: result
|
51
|
+
}
|
52
|
+
|
53
|
+
metadata = metadata.merge(operation: "execute_tool")
|
54
|
+
format_success(data, metadata)
|
55
|
+
end
|
56
|
+
|
57
|
+
def format_resource_content(content, metadata = {})
|
58
|
+
data = {
|
59
|
+
content: content
|
60
|
+
}
|
61
|
+
|
62
|
+
metadata = metadata.merge(operation: "read_resource")
|
63
|
+
format_success(data, metadata)
|
64
|
+
end
|
65
|
+
|
66
|
+
def format_prompt_result(result, metadata = {})
|
67
|
+
data = {
|
68
|
+
result: result
|
69
|
+
}
|
70
|
+
|
71
|
+
metadata = metadata.merge(operation: "get_prompt")
|
72
|
+
format_success(data, metadata)
|
73
|
+
end
|
74
|
+
|
75
|
+
def format_server_info(info, metadata = {})
|
76
|
+
data = {
|
77
|
+
server_info: info
|
78
|
+
}
|
79
|
+
|
80
|
+
metadata = metadata.merge(operation: "server_info")
|
81
|
+
format_success(data, metadata)
|
82
|
+
end
|
83
|
+
|
84
|
+
def format_config_list(servers, metadata = {})
|
85
|
+
server_list = servers.map do |name, config|
|
86
|
+
{
|
87
|
+
name: name,
|
88
|
+
transport: config.transport,
|
89
|
+
description: build_server_description(config)
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
data = {
|
94
|
+
servers: server_list,
|
95
|
+
count: servers.length
|
96
|
+
}
|
97
|
+
|
98
|
+
metadata = metadata.merge(operation: "config_list")
|
99
|
+
format_success(data, metadata)
|
100
|
+
end
|
101
|
+
|
102
|
+
def format_config_details(server_config, metadata = {})
|
103
|
+
data = {
|
104
|
+
server_config: server_config.to_h
|
105
|
+
}
|
106
|
+
|
107
|
+
metadata = metadata.merge(operation: "config_show")
|
108
|
+
format_success(data, metadata)
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def to_json(data)
|
114
|
+
if pretty
|
115
|
+
JSON.pretty_generate(data, {
|
116
|
+
indent: " ",
|
117
|
+
space: " ",
|
118
|
+
object_nl: "\n",
|
119
|
+
array_nl: "\n"
|
120
|
+
})
|
121
|
+
else
|
122
|
+
JSON.generate(data)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def normalize_list_data(data)
|
127
|
+
case data
|
128
|
+
when Hash
|
129
|
+
data.fetch("items", [])
|
130
|
+
when Array
|
131
|
+
data
|
132
|
+
when NilClass
|
133
|
+
[]
|
134
|
+
else
|
135
|
+
[data]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def build_server_description(config)
|
140
|
+
case config.transport
|
141
|
+
when "stdio"
|
142
|
+
"#{config.command.join(' ')} (stdio)"
|
143
|
+
when "sse"
|
144
|
+
"#{config.url} (sse)"
|
145
|
+
when "websocket"
|
146
|
+
"#{config.url} (websocket)"
|
147
|
+
else
|
148
|
+
"#{config.transport} transport"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|