swagger_mcp_tool 0.1.1
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/README.md +594 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/bin/swagger_mcp_server +45 -0
- data/lib/swagger_mcp_tool/api_client.rb +154 -0
- data/lib/swagger_mcp_tool/auth_handler.rb +82 -0
- data/lib/swagger_mcp_tool/config.rb +143 -0
- data/lib/swagger_mcp_tool/helpers/parameter_processor.rb +84 -0
- data/lib/swagger_mcp_tool/helpers/request.rb +114 -0
- data/lib/swagger_mcp_tool/helpers/request_body_processor.rb +66 -0
- data/lib/swagger_mcp_tool/helpers/swagger_validator.rb +42 -0
- data/lib/swagger_mcp_tool/helpers/tool_builder.rb +43 -0
- data/lib/swagger_mcp_tool/helpers/tool_register.rb +40 -0
- data/lib/swagger_mcp_tool/logging.rb +61 -0
- data/lib/swagger_mcp_tool/server.rb +254 -0
- data/lib/swagger_mcp_tool/swagger_client.rb +116 -0
- data/lib/swagger_mcp_tool/tool_generator.rb +85 -0
- data/lib/swagger_mcp_tool/tool_registry.rb +211 -0
- data/lib/swagger_mcp_tool/version.rb +5 -0
- data/lib/swagger_mcp_tool.rb +21 -0
- metadata +221 -0
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'net/http'
|
5
|
+
require 'openssl'
|
6
|
+
require 'json'
|
7
|
+
require 'mcp'
|
8
|
+
require_relative 'helpers/request'
|
9
|
+
require_relative 'logging'
|
10
|
+
require_relative 'config'
|
11
|
+
|
12
|
+
module SwaggerMCPTool
|
13
|
+
# The SwaggerMCPTool module provides tools and utilities for interacting with APIs
|
14
|
+
# using Swagger/OpenAPI specifications. It includes classes and helpers for making
|
15
|
+
# HTTP requests, handling responses, logging, and managing configuration.
|
16
|
+
#
|
17
|
+
# This module is intended to be used as part of the swagger_mcp_tool library,
|
18
|
+
# facilitating API client operations with robust error handling and logging.
|
19
|
+
#
|
20
|
+
# Example usage:
|
21
|
+
# client = SwaggerMCPTool::ApiClient.new("https://api.example.com")
|
22
|
+
# response = client.make_request(:get, "/v1/resource", { id: 123 })
|
23
|
+
#
|
24
|
+
# @see SwaggerMCPTool::ApiClient
|
25
|
+
# @see SwaggerMCPTool::Helpers::Request
|
26
|
+
# @see SwaggerMCPTool::Logging
|
27
|
+
class ApiClient
|
28
|
+
include SwaggerMCPTool::Helpers::Request
|
29
|
+
include SwaggerMCPTool::Logging
|
30
|
+
|
31
|
+
def initialize(base_url)
|
32
|
+
@base_url = base_url.chomp('/')
|
33
|
+
@config = Config.instance
|
34
|
+
end
|
35
|
+
|
36
|
+
def make_request(method, path, params = {}, headers = {}, server_context = {})
|
37
|
+
request_context = build_request_context(method, path, params, headers, server_context)
|
38
|
+
|
39
|
+
log_request_details(request_context)
|
40
|
+
|
41
|
+
response = execute_http_request(request_context)
|
42
|
+
process_response(response)
|
43
|
+
rescue StandardError => e
|
44
|
+
handle_request_error(e)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def execute_http_request(context)
|
50
|
+
processed_path, remaining_params = process_path_parameters(context[:original_path], context[:params])
|
51
|
+
uri = build_request_uri(processed_path, context[:method], remaining_params)
|
52
|
+
|
53
|
+
@config.logger.debug "Final URI: #{uri}"
|
54
|
+
|
55
|
+
http_client = create_http_client(uri)
|
56
|
+
request = create_http_request(context[:method], uri, remaining_params)
|
57
|
+
|
58
|
+
add_headers_to_request(request, context[:headers], context[:auth_token])
|
59
|
+
|
60
|
+
log_request_execution(context[:method], uri)
|
61
|
+
http_client.request(request)
|
62
|
+
end
|
63
|
+
|
64
|
+
def create_http_client(uri)
|
65
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
66
|
+
|
67
|
+
configure_ssl(http, uri)
|
68
|
+
configure_timeouts(http)
|
69
|
+
|
70
|
+
http
|
71
|
+
end
|
72
|
+
|
73
|
+
def configure_ssl(http, uri)
|
74
|
+
return unless uri.scheme == 'https'
|
75
|
+
|
76
|
+
http.use_ssl = true
|
77
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
78
|
+
end
|
79
|
+
|
80
|
+
def configure_timeouts(http)
|
81
|
+
http.open_timeout = 10
|
82
|
+
http.read_timeout = 30
|
83
|
+
end
|
84
|
+
|
85
|
+
def process_response(response)
|
86
|
+
if response.is_a?(Net::HTTPSuccess)
|
87
|
+
process_success_response(response)
|
88
|
+
else
|
89
|
+
process_error_response(response)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def process_success_response(response)
|
94
|
+
parsed_response = parse_response_body(response.body)
|
95
|
+
|
96
|
+
@config.logger.debug "API Response: #{parsed_response.inspect}"
|
97
|
+
|
98
|
+
create_mcp_response(format_success_response(parsed_response))
|
99
|
+
end
|
100
|
+
|
101
|
+
def process_error_response(response)
|
102
|
+
error_message = build_error_message(response)
|
103
|
+
|
104
|
+
@config.logger.error "API request failed: #{response.code} #{response.message}"
|
105
|
+
@config.logger.error "Response body: #{response.body}"
|
106
|
+
|
107
|
+
create_mcp_response(error_message)
|
108
|
+
end
|
109
|
+
|
110
|
+
def parse_response_body(body)
|
111
|
+
JSON.parse(body)
|
112
|
+
rescue JSON::ParserError
|
113
|
+
body
|
114
|
+
end
|
115
|
+
|
116
|
+
def format_success_response(parsed_response)
|
117
|
+
case parsed_response
|
118
|
+
when Hash, Array
|
119
|
+
JSON.pretty_generate(parsed_response)
|
120
|
+
else
|
121
|
+
parsed_response.to_s
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def build_error_message(response)
|
126
|
+
error_message = "Error: #{response.code} #{response.message}\n"
|
127
|
+
|
128
|
+
error_body = parse_response_body(response.body)
|
129
|
+
error_message += case error_body
|
130
|
+
when Hash, Array
|
131
|
+
JSON.pretty_generate(error_body)
|
132
|
+
else
|
133
|
+
error_body.to_s
|
134
|
+
end
|
135
|
+
|
136
|
+
error_message
|
137
|
+
end
|
138
|
+
|
139
|
+
def create_mcp_response(text)
|
140
|
+
MCP::Tool::Response.new([{ type: 'text', text: text }])
|
141
|
+
end
|
142
|
+
|
143
|
+
def handle_request_error(error)
|
144
|
+
@config.logger.error "Error making API request: #{error.message}"
|
145
|
+
@config.logger.error error.backtrace.join("\n")
|
146
|
+
|
147
|
+
create_mcp_response("Error: #{error.message}")
|
148
|
+
end
|
149
|
+
|
150
|
+
def sanitize_headers_for_logging(headers)
|
151
|
+
headers.inspect.gsub(/"Authorization"=>"[^"]+"/, '"Authorization"=>"[REDACTED]"')
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwaggerMCPTool
|
4
|
+
class AuthHandler
|
5
|
+
# Initialize with optional default auth settings
|
6
|
+
def initialize(default_auth = {})
|
7
|
+
@config = Config.instance
|
8
|
+
@default_auth = default_auth || {}
|
9
|
+
@auth_tokens = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
# Set auth token for a user
|
13
|
+
def set_auth_token(user_id, token)
|
14
|
+
@auth_tokens[user_id.to_s] = token
|
15
|
+
@config.logger.info "Auth token set for user: #{user_id}"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Get auth token for a user
|
19
|
+
def get_auth_token(user_id)
|
20
|
+
@auth_tokens[user_id.to_s] || @default_auth[:token] || @config.default_token
|
21
|
+
end
|
22
|
+
|
23
|
+
# Clear auth token for a user
|
24
|
+
def clear_auth_token(user_id)
|
25
|
+
@auth_tokens.delete(user_id.to_s)
|
26
|
+
@config.logger.info "Auth token cleared for user: #{user_id}"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Check if a user has an auth token
|
30
|
+
def has_auth_token?(user_id)
|
31
|
+
@auth_tokens.key?(user_id.to_s) || @default_auth[:token].nil? == false || @config.default_token.nil? == false
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get auth headers for a request
|
35
|
+
def get_auth_headers(user_id, api_key_header = nil)
|
36
|
+
headers = {}
|
37
|
+
|
38
|
+
token = get_auth_token(user_id)
|
39
|
+
if token
|
40
|
+
headers['Authorization'] = token.start_with?('Bearer ', 'Token ', 'Basic ') ? token : "Bearer #{token}"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Add API key if provided
|
44
|
+
headers[api_key_header] = @default_auth[:api_key] if api_key_header && @default_auth[:api_key]
|
45
|
+
|
46
|
+
headers
|
47
|
+
end
|
48
|
+
|
49
|
+
# Process auth from Swagger security definitions
|
50
|
+
def process_swagger_security(swagger_spec)
|
51
|
+
return {} unless swagger_spec['securityDefinitions'] || swagger_spec['components']&.dig('securitySchemes')
|
52
|
+
|
53
|
+
# Get security definitions based on Swagger/OpenAPI version
|
54
|
+
security_defs = if swagger_spec['swagger'] == '2.0'
|
55
|
+
swagger_spec['securityDefinitions'] || {}
|
56
|
+
else
|
57
|
+
swagger_spec['components']&.dig('securitySchemes') || {}
|
58
|
+
end
|
59
|
+
|
60
|
+
auth_config = {}
|
61
|
+
|
62
|
+
security_defs.each_value do |definition|
|
63
|
+
type = definition['type']&.downcase
|
64
|
+
|
65
|
+
case type
|
66
|
+
when 'apikey'
|
67
|
+
auth_config[:api_key_name] = definition['name']
|
68
|
+
auth_config[:api_key_in] = definition['in'] # 'header' or 'query'
|
69
|
+
when 'oauth2', 'http'
|
70
|
+
scheme = definition['scheme']&.downcase
|
71
|
+
if scheme == 'bearer' || definition['bearerFormat'] || type == 'oauth2'
|
72
|
+
auth_config[:token_type] = 'Bearer'
|
73
|
+
elsif scheme == 'basic'
|
74
|
+
auth_config[:token_type] = 'Basic'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
auth_config
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'singleton'
|
5
|
+
|
6
|
+
module SwaggerMCPTool
|
7
|
+
# Configuration management for SwaggerMCPTool
|
8
|
+
#
|
9
|
+
# This class uses the Singleton pattern to ensure a single configuration
|
10
|
+
# instance throughout the application lifecycle.
|
11
|
+
class Config
|
12
|
+
include Singleton
|
13
|
+
|
14
|
+
attr_accessor :server_port, :server_bind, :server_type,
|
15
|
+
:log_level, :log_file, :swagger_url, :mcp_name,
|
16
|
+
:mcp_name_for_human, :mcp_description_for_human,
|
17
|
+
:mcp_description_for_model, :auth_type, :default_token,
|
18
|
+
:prompts, :resources, :auth_header_name, :tools
|
19
|
+
|
20
|
+
DEFAULT_SERVER_PORT = 3001
|
21
|
+
DEFAULT_SERVER_BIND = '0.0.0.0'
|
22
|
+
DEFAULT_SERVER_TYPE = :puma
|
23
|
+
DEFAULT_LOG_LEVEL = Logger::INFO
|
24
|
+
DEFAULT_AUTH_HEADER_NAME = 'Token'
|
25
|
+
DEFAULT_SWAGGER_URL = 'https://petstore.swagger.io/v2/swagger.json'
|
26
|
+
DEFAULT_MCP_NAME = 'swagger_api_tools'
|
27
|
+
DEFAULT_MCP_NAME_FOR_HUMAN = 'Swagger API Tools'
|
28
|
+
DEFAULT_MCP_DESCRIPTION_FOR_HUMAN = 'Tools for interacting with APIs via Swagger/OpenAPI specifications'
|
29
|
+
DEFAULT_MCP_DESCRIPTION_FOR_MODEL = 'This MCP server provides tools for interacting with APIs via Swagger/OpenAPI specifications.'
|
30
|
+
DEFAULT_AUTH_TYPE = 'none'
|
31
|
+
DEFAULT_LOG_FILE = nil
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
# Server settings
|
35
|
+
setup_server_defaults
|
36
|
+
|
37
|
+
# Swagger settings
|
38
|
+
setup_swagger_defaults
|
39
|
+
|
40
|
+
# MCP settings
|
41
|
+
setup_mcp_defaults
|
42
|
+
|
43
|
+
# Auth settings
|
44
|
+
setup_auth_defaults
|
45
|
+
|
46
|
+
# Tools, Prompts and resources
|
47
|
+
@tools = []
|
48
|
+
@resources = []
|
49
|
+
@prompts = [] # Will be generated lazily
|
50
|
+
end
|
51
|
+
|
52
|
+
def setup_server_defaults
|
53
|
+
@server_port = DEFAULT_SERVER_PORT
|
54
|
+
@server_bind = DEFAULT_SERVER_BIND
|
55
|
+
@server_type = DEFAULT_SERVER_TYPE
|
56
|
+
@log_level = DEFAULT_LOG_LEVEL
|
57
|
+
@log_file = nil
|
58
|
+
@auth_header_name = DEFAULT_AUTH_HEADER_NAME
|
59
|
+
end
|
60
|
+
|
61
|
+
def setup_swagger_defaults
|
62
|
+
@swagger_url = DEFAULT_SWAGGER_URL
|
63
|
+
end
|
64
|
+
|
65
|
+
def setup_mcp_defaults
|
66
|
+
@mcp_name = DEFAULT_MCP_NAME
|
67
|
+
@mcp_name_for_human = DEFAULT_MCP_NAME_FOR_HUMAN
|
68
|
+
@mcp_description_for_human = DEFAULT_MCP_DESCRIPTION_FOR_HUMAN
|
69
|
+
@mcp_description_for_model = DEFAULT_MCP_DESCRIPTION_FOR_MODEL
|
70
|
+
end
|
71
|
+
|
72
|
+
def setup_auth_defaults
|
73
|
+
@auth_type = DEFAULT_AUTH_TYPE
|
74
|
+
@default_token = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
# Get the logger
|
78
|
+
def logger
|
79
|
+
@logger ||= create_logger
|
80
|
+
end
|
81
|
+
|
82
|
+
def create_logger
|
83
|
+
create_file_logger
|
84
|
+
rescue StandardError => e
|
85
|
+
logger = Logger.new($stdout)
|
86
|
+
logger.level = @log_level
|
87
|
+
logger.warn "Could not create log file #{@log_file} (#{e.message}), using stderr for logging"
|
88
|
+
logger
|
89
|
+
end
|
90
|
+
|
91
|
+
def create_file_logger
|
92
|
+
target = case @log_file
|
93
|
+
when $stdout, $stdout, 'STDOUT', 'stdout'
|
94
|
+
$stdout
|
95
|
+
when $stderr, $stderr, 'STDERR', 'stderr'
|
96
|
+
$stderr
|
97
|
+
when nil, ''
|
98
|
+
$stderr
|
99
|
+
else
|
100
|
+
@log_file
|
101
|
+
end
|
102
|
+
logger = Logger.new(target)
|
103
|
+
logger.level = @log_level
|
104
|
+
logger
|
105
|
+
end
|
106
|
+
|
107
|
+
# Configure the instance
|
108
|
+
def self.configure
|
109
|
+
return unless block_given?
|
110
|
+
|
111
|
+
config_instance = instance
|
112
|
+
yield config_instance
|
113
|
+
log_configuration_details(config_instance)
|
114
|
+
config_instance
|
115
|
+
rescue StandardError => e
|
116
|
+
config_instance.logger.error "Configuration failed: #{e.message}"
|
117
|
+
raise e
|
118
|
+
end
|
119
|
+
|
120
|
+
# Get server context
|
121
|
+
def to_context(request)
|
122
|
+
auth_header_name = self.auth_header_name || 'Authorization'
|
123
|
+
{
|
124
|
+
user_id: request.env['HTTP_X_USER_ID'] || '1',
|
125
|
+
auth_header_name.to_sym => request.env['HTTP_X_AUTH_TOKEN'],
|
126
|
+
username: request.env['HTTP_X_USERNAME'],
|
127
|
+
email: request.env['HTTP_X_EMAIL']
|
128
|
+
}
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.log_configuration_details(config_instance)
|
132
|
+
logger = config_instance.logger
|
133
|
+
logger.info '=== CONFIGURATION COMPLETE ==='
|
134
|
+
logger.info 'Final config values:'
|
135
|
+
logger.info " server_port: #{config_instance.server_port}"
|
136
|
+
logger.info " mcp_name: #{config_instance.mcp_name}"
|
137
|
+
logger.info " auth_header_name: #{config_instance.auth_header_name}"
|
138
|
+
logger.info " prompts: #{config_instance.prompts}"
|
139
|
+
end
|
140
|
+
|
141
|
+
private_class_method :log_configuration_details
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwaggerMCPTool
|
4
|
+
module Helpers
|
5
|
+
module ParameterProcessor
|
6
|
+
SWAGGER_TO_JSON_SCHEMA_TYPES = {
|
7
|
+
'file' => 'string',
|
8
|
+
'integer' => 'integer',
|
9
|
+
'int' => 'integer',
|
10
|
+
'int32' => 'integer',
|
11
|
+
'int64' => 'integer',
|
12
|
+
'number' => 'number',
|
13
|
+
'float' => 'number',
|
14
|
+
'double' => 'number',
|
15
|
+
'boolean' => 'boolean',
|
16
|
+
'bool' => 'boolean',
|
17
|
+
'array' => 'array',
|
18
|
+
'object' => 'object'
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def add_parameters_to_tool(tool_definition, operation)
|
24
|
+
parameters = operation['parameters']
|
25
|
+
return unless parameters.is_a?(Array)
|
26
|
+
|
27
|
+
parameters.each do |param|
|
28
|
+
process_single_parameter(tool_definition, param)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def process_single_parameter(tool_definition, param)
|
33
|
+
return if skip_parameter?(param)
|
34
|
+
|
35
|
+
resolved_param = resolve_parameter_reference(param)
|
36
|
+
return unless valid_parameter?(resolved_param)
|
37
|
+
|
38
|
+
add_parameter_to_schema(tool_definition, resolved_param)
|
39
|
+
end
|
40
|
+
|
41
|
+
def skip_parameter?(param)
|
42
|
+
param['in'] == 'header'
|
43
|
+
end
|
44
|
+
|
45
|
+
def resolve_parameter_reference(param)
|
46
|
+
return param unless param['$ref']
|
47
|
+
|
48
|
+
resolve_swagger_reference(param['$ref'])
|
49
|
+
rescue StandardError => e
|
50
|
+
@config.logger.debug "Failed to resolve parameter reference: #{e.message}"
|
51
|
+
param
|
52
|
+
end
|
53
|
+
|
54
|
+
def resolve_swagger_reference(ref_path)
|
55
|
+
return nil unless ref_path.is_a?(String)
|
56
|
+
|
57
|
+
path_parts = ref_path.sub('#/', '').split('/')
|
58
|
+
path_parts.reduce(@swagger_client.swagger_spec) { |acc, part| acc[part] }
|
59
|
+
end
|
60
|
+
|
61
|
+
def valid_parameter?(param)
|
62
|
+
param.is_a?(Hash) && param['name'].is_a?(String)
|
63
|
+
end
|
64
|
+
|
65
|
+
def add_parameter_to_schema(tool_definition, param)
|
66
|
+
param_name = param['name']
|
67
|
+
param_type = normalize_parameter_type(param['type'])
|
68
|
+
|
69
|
+
tool_definition['parameters']['properties'][param_name] = {
|
70
|
+
'type' => param_type,
|
71
|
+
'description' => param['description'] || param_name
|
72
|
+
}
|
73
|
+
|
74
|
+
return unless param['required']
|
75
|
+
|
76
|
+
tool_definition['parameters']['required'] << param_name
|
77
|
+
end
|
78
|
+
|
79
|
+
def normalize_parameter_type(swagger_type)
|
80
|
+
SWAGGER_TO_JSON_SCHEMA_TYPES[swagger_type] || 'string'
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'net/http'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module SwaggerMCPTool
|
8
|
+
module Helpers
|
9
|
+
# Helper for HTTP request building and execution
|
10
|
+
module Request
|
11
|
+
def build_request_context(method, path, params, headers, server_context)
|
12
|
+
{
|
13
|
+
method: method.downcase,
|
14
|
+
original_path: path,
|
15
|
+
params: params,
|
16
|
+
headers: headers,
|
17
|
+
auth_token: server_context[@config.auth_header_name]
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_http_request(method, uri, params)
|
22
|
+
req_method = request_methods(method.downcase)
|
23
|
+
request = req_method.new(uri.request_uri)
|
24
|
+
|
25
|
+
set_json_body(request, params) if body_required?(method.downcase)
|
26
|
+
request
|
27
|
+
end
|
28
|
+
|
29
|
+
def body_required?(method)
|
30
|
+
%w[post put patch].include?(method)
|
31
|
+
end
|
32
|
+
|
33
|
+
def request_methods(method)
|
34
|
+
{
|
35
|
+
get: Net::HTTP::Get,
|
36
|
+
post: Net::HTTP::Post,
|
37
|
+
put: Net::HTTP::Put,
|
38
|
+
delete: Net::HTTP::Delete,
|
39
|
+
patch: Net::HTTP::Patch
|
40
|
+
}[method] || Net::HTTP::Get
|
41
|
+
end
|
42
|
+
|
43
|
+
def set_json_body(request, params)
|
44
|
+
return if params.empty?
|
45
|
+
|
46
|
+
request.body = params.to_json
|
47
|
+
request.content_type = 'application/json'
|
48
|
+
end
|
49
|
+
|
50
|
+
def build_request_uri(path, method, params)
|
51
|
+
uri = URI("#{@base_url}#{path}")
|
52
|
+
|
53
|
+
# Add query parameters only for GET requests
|
54
|
+
uri.query = URI.encode_www_form(params) if method == 'get' && !params.empty?
|
55
|
+
|
56
|
+
uri
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_headers_to_request(request, headers, auth_token)
|
60
|
+
# Add custom headers
|
61
|
+
headers.each { |key, value| request[key] = value }
|
62
|
+
|
63
|
+
# Add authorization header if available
|
64
|
+
add_authorization_header(request, auth_token)
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_authorization_header(request, auth_token)
|
68
|
+
return unless auth_token && @config.auth_header_name
|
69
|
+
|
70
|
+
request[@config.auth_header_name] = auth_token
|
71
|
+
@config.logger.info 'Added authorization header for request'
|
72
|
+
end
|
73
|
+
|
74
|
+
def process_path_parameters(path, params)
|
75
|
+
return [path, params] unless path_has_placeholders?(path)
|
76
|
+
|
77
|
+
processed_path = path.dup
|
78
|
+
used_params = substitute_path_placeholders!(processed_path, params)
|
79
|
+
remaining_params = filter_unused_params(params, used_params)
|
80
|
+
|
81
|
+
[processed_path, remaining_params]
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def path_has_placeholders?(path)
|
87
|
+
path.include?('{')
|
88
|
+
end
|
89
|
+
|
90
|
+
def substitute_path_placeholders!(path, params)
|
91
|
+
used_params = []
|
92
|
+
|
93
|
+
params.each do |key, value|
|
94
|
+
placeholder = "{#{key}}"
|
95
|
+
|
96
|
+
if path.include?(placeholder)
|
97
|
+
path.gsub!(placeholder, encode_path_value(value))
|
98
|
+
used_params << key
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
used_params
|
103
|
+
end
|
104
|
+
|
105
|
+
def encode_path_value(value)
|
106
|
+
URI.encode_www_form_component(value.to_s)
|
107
|
+
end
|
108
|
+
|
109
|
+
def filter_unused_params(params, used_params)
|
110
|
+
params.reject { |key, _| used_params.include?(key) }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwaggerMCPTool
|
4
|
+
module Helpers
|
5
|
+
module RequestBodyProcessor
|
6
|
+
private
|
7
|
+
|
8
|
+
def add_request_body_to_tool(tool_definition, operation)
|
9
|
+
request_body = operation['requestBody']
|
10
|
+
return unless request_body_present?(request_body)
|
11
|
+
|
12
|
+
process_request_body_content(tool_definition, request_body['content'])
|
13
|
+
rescue StandardError => e
|
14
|
+
@config.logger.debug "Failed to process request body: #{e.message}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def request_body_present?(request_body)
|
18
|
+
request_body && request_body['content']
|
19
|
+
end
|
20
|
+
|
21
|
+
def process_request_body_content(tool_definition, content)
|
22
|
+
return unless content.is_a?(Hash)
|
23
|
+
|
24
|
+
content_type = select_content_type(content)
|
25
|
+
return unless content_type
|
26
|
+
|
27
|
+
schema = extract_schema_from_content(content, content_type)
|
28
|
+
add_schema_properties_to_tool(tool_definition, schema) if schema
|
29
|
+
end
|
30
|
+
|
31
|
+
def select_content_type(content)
|
32
|
+
content.keys.first
|
33
|
+
end
|
34
|
+
|
35
|
+
def extract_schema_from_content(content, content_type)
|
36
|
+
content.dig(content_type, 'schema')
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_schema_properties_to_tool(tool_definition, schema)
|
40
|
+
properties = schema['properties']
|
41
|
+
return unless properties.is_a?(Hash)
|
42
|
+
|
43
|
+
properties.each do |name, property|
|
44
|
+
add_property_to_tool(tool_definition, name, property)
|
45
|
+
end
|
46
|
+
|
47
|
+
add_required_fields_to_tool(tool_definition, schema['required'])
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_property_to_tool(tool_definition, name, property)
|
51
|
+
tool_definition['parameters']['properties'][name] = {
|
52
|
+
'type' => property['type'] || 'string',
|
53
|
+
'description' => property['description'] || name
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_required_fields_to_tool(tool_definition, required_fields)
|
58
|
+
return unless required_fields.is_a?(Array)
|
59
|
+
|
60
|
+
required_fields.each do |field_name|
|
61
|
+
tool_definition['parameters']['required'] << field_name
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json-schema'
|
4
|
+
|
5
|
+
module SwaggerMCPTool
|
6
|
+
module Helpers
|
7
|
+
# Provides validation methods for Swagger (OpenAPI) specification structures.
|
8
|
+
# This module includes methods to validate the overall structure of a Swagger spec,
|
9
|
+
# ensure required keys are present, and check the structure of the 'paths' section.
|
10
|
+
# Intended to be included in classes that require Swagger spec validation logic.
|
11
|
+
#
|
12
|
+
# Example usage:
|
13
|
+
# include SwaggerValidator
|
14
|
+
# validate_swagger_spec!(swagger_spec_hash)
|
15
|
+
#
|
16
|
+
# Raises ArgumentError if the spec or paths are invalid.
|
17
|
+
#
|
18
|
+
# @see https://swagger.io/specification/
|
19
|
+
module SwaggerValidator
|
20
|
+
def validate_swagger_spec!(swagger_spec)
|
21
|
+
validate_spec_structure!(swagger_spec)
|
22
|
+
validate_paths_structure!(swagger_spec['paths'])
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate_spec_structure!(swagger_spec)
|
26
|
+
raise ArgumentError, 'Swagger spec must be a Hash' unless swagger_spec.is_a?(Hash)
|
27
|
+
|
28
|
+
return if swagger_spec.key?('paths')
|
29
|
+
|
30
|
+
raise ArgumentError, 'Swagger spec must contain paths'
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_paths_structure!(paths)
|
34
|
+
raise ArgumentError, 'Paths must be a Hash' unless paths.is_a?(Hash)
|
35
|
+
|
36
|
+
return unless paths.empty?
|
37
|
+
|
38
|
+
@config.logger.warn 'No paths found in Swagger spec'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|