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.
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwaggerMCPTool
4
+ module Helpers
5
+ module ToolBuilder
6
+ private
7
+
8
+ def create_base_tool_definition(path, method, operation)
9
+ {
10
+ 'name' => generate_operation_id(path, method, operation),
11
+ 'description' => generate_description(path, method, operation),
12
+ 'method' => method,
13
+ 'path' => path,
14
+ 'parameters' => create_empty_parameters_schema
15
+ }
16
+ end
17
+
18
+ def generate_operation_id(path, method, operation)
19
+ operation['operationId'] || generate_default_operation_id(path, method)
20
+ end
21
+
22
+ def generate_default_operation_id(path, method)
23
+ "#{method}_#{sanitize_path_for_id(path)}"
24
+ end
25
+
26
+ def sanitize_path_for_id(path)
27
+ path.gsub(/[^\w]/, '_')
28
+ end
29
+
30
+ def generate_description(path, method, operation)
31
+ operation['summary'] || operation['description'] || "#{method.upcase} #{path}"
32
+ end
33
+
34
+ def create_empty_parameters_schema
35
+ {
36
+ 'type' => 'object',
37
+ 'properties' => {},
38
+ 'required' => []
39
+ }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwaggerMCPTool
4
+ module Helpers
5
+ module ToolRegister
6
+ def tool_registry
7
+ SwaggerMCPTool::ToolRegistry.instance
8
+ end
9
+
10
+ def setup_tool_registry
11
+ registry = tool_registry
12
+ registry.setup(@config) # <-- This is where setup gets called
13
+ @logger = @config.logger
14
+ log_message('Tool registry setup complete')
15
+ end
16
+
17
+ def initialize_tools
18
+ generate_tools_from_swagger_url
19
+ end
20
+
21
+ # Generate tools from a Swagger URL
22
+ def generate_tools_from_swagger_url
23
+ @config.logger.info "Generating tools from Swagger URL: #{@config.swagger_url}"
24
+ # Create a Swagger client
25
+ swagger_client = SwaggerClient.new(@config.swagger_url)
26
+
27
+ # Create a tool generator
28
+ tool_generator = ToolGenerator.new(swagger_client)
29
+
30
+ # Generate tools
31
+ tools = tool_generator.generate_tools
32
+
33
+ # Register the tools dynamically (simplified)
34
+ tool_registry.register_dynamic_tools(tools, swagger_client.base_url)
35
+ rescue StandardError => e
36
+ log_and_raise_error(e)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The SwaggerMCPTool module provides core functionality for the Swagger MCP Tool gem.
4
+ # It serves as a namespace for organizing related components, such as logging utilities,
5
+ # configuration management, and integration helpers for managing Swagger-based MCP services.
6
+
7
+ module SwaggerMCPTool
8
+ # SwaggerMCPTool::Logging for logging-related methods and usage.
9
+ module Logging
10
+ def log_server_initialization
11
+ logger.info '=== SERVER INITIALIZATION ==='
12
+
13
+ server_config_items.each do |label, value|
14
+ logger.info "#{label}: #{value}"
15
+ end
16
+
17
+ logger.info '================================'
18
+ end
19
+
20
+ def log_and_raise_error(exception)
21
+ logger.error(exception.message)
22
+ raise exception
23
+ end
24
+
25
+ def log_message(message)
26
+ logger.info(message)
27
+ end
28
+
29
+ def log_request_details(context)
30
+ logger.debug 'API Request Details:'
31
+ logger.debug " Method: #{context[:method].upcase}"
32
+ logger.debug " Path: #{context[:original_path]}"
33
+ logger.debug " Params: #{context[:params].inspect}"
34
+ logger.debug " Headers: #{sanitize_headers_for_logging(context[:headers])}"
35
+ end
36
+
37
+ def log_request_execution(method, uri)
38
+ logger.info "Making #{method.upcase} request to #{uri.host}#{uri.path}"
39
+ end
40
+
41
+ private
42
+
43
+ def server_config_items
44
+ [
45
+ ['Server port', config.server_port],
46
+ ['Server bind', config.server_bind],
47
+ ['Swagger URL', config.swagger_url],
48
+ ['MCP name', config.mcp_name],
49
+ ['Auth header name', config.auth_header_name]
50
+ ]
51
+ end
52
+
53
+ def logger
54
+ config.logger
55
+ end
56
+
57
+ def config
58
+ @config
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+ require 'sinatra/json'
5
+ require 'mcp'
6
+ require 'json'
7
+ require 'logger'
8
+ require 'byebug'
9
+ require_relative 'tool_registry'
10
+ require_relative 'logging'
11
+ require_relative 'config'
12
+ require_relative 'helpers/tool_register'
13
+
14
+ module SwaggerMCPTool
15
+ # The Server class is the main Sinatra-based HTTP server for the SwaggerMCPTool.
16
+ # It provides endpoints for manifest retrieval, tool generation, authentication token management,
17
+ # and health checks. The server dynamically generates tools from a Swagger/OpenAPI specification,
18
+ # manages user authentication, and integrates with the MCP (Model Control Protocol) server.
19
+ #
20
+ # Key responsibilities:
21
+ # - Initializes configuration and tool registry on startup.
22
+ # - Exposes RESTful endpoints for manifest, MCP requests, tool generation, and health checks.
23
+ # - Handles CORS and server configuration.
24
+ # - Dynamically generates and registers tools from a Swagger URL.
25
+ # - Manages user authentication tokens and server context.
26
+ #
27
+ # Endpoints:
28
+ # - GET /mcp/manifest : Returns the MCP manifest for integration.
29
+ # - POST /mcp : Handles MCP requests with dynamic tools.
30
+ # - POST /set_auth_token : Sets authentication tokens for users.
31
+ # - POST /generate_tools : Generates tools from a provided Swagger URL.
32
+ # - GET /logo.png : Serves the logo image.
33
+ # - GET /health : Health check and server status.
34
+ #
35
+ # Usage:
36
+ # SwaggerMCPTool::Server.start
37
+ #
38
+ # Dependencies:
39
+ # - Sinatra::Base
40
+ # - SwaggerMCPTool::Config
41
+ # - SwaggerMCPTool::ToolRegistry
42
+ # - SwaggerClient
43
+ # - ToolGenerator
44
+ # - MCP::Server
45
+ #
46
+ # Configuration is managed via the Config singleton, and tools are registered
47
+ # dynamically via the ToolRegistry singleton.
48
+ class Server < Sinatra::Base
49
+ attr_reader :config, :dynamic_tools
50
+
51
+ include Logging
52
+ include Helpers::ToolRegister
53
+
54
+ # Initialize with configuration
55
+ def initialize(app = nil)
56
+ super(app)
57
+ @config = Config.instance
58
+
59
+ # Debug configuration values
60
+ log_server_initialization
61
+ # Generate tools on startup
62
+ setup_tool_registry
63
+ initialize_tools
64
+ # Configure server settings
65
+ configure_server
66
+ end
67
+
68
+ # def tool_registry
69
+ # SwaggerMCPTool::ToolRegistry.instance
70
+ # end
71
+
72
+ # def setup_tool_registry
73
+ # registry = tool_registry
74
+ # registry.setup(@config) # <-- This is where setup gets called
75
+ # @logger = @config.logger
76
+ # log_message('Tool registry setup complete')
77
+ # end
78
+
79
+ # def initialize_tools
80
+ # generate_tools_from_swagger_url
81
+ # end
82
+
83
+ # Start the server
84
+ def self.start
85
+ # self.define_routes
86
+ new
87
+ run!
88
+ end
89
+
90
+ def configure_server
91
+ self.class.set :server, @config.server_type
92
+ self.class.set :bind, @config.server_bind
93
+ self.class.set :port, @config.server_port
94
+
95
+ # Enable CORS
96
+ enable_cors
97
+
98
+ # Add options headers
99
+ add_options
100
+ end
101
+
102
+ def enable_cors
103
+ self.class.before do
104
+ headers 'Access-Control-Allow-Origin' => '*',
105
+ 'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS',
106
+ 'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-User-ID, Auth-Token, X-Username, X-Email'
107
+ end
108
+ end
109
+
110
+ def add_options
111
+ self.class.options '*' do
112
+ response.headers['Allow'] = 'GET, POST, OPTIONS'
113
+ response.headers['Access-Control-Allow-Headers'] =
114
+ 'Authorization, Content-Type, Accept, X-User-ID, Auth-Token, X-Username, X-Email'
115
+ 200
116
+ end
117
+ end
118
+
119
+ # private
120
+
121
+ # Define server routes
122
+ # def define_routes
123
+ # MCP manifest endpoint
124
+ get '/mcp/manifest' do
125
+ manifest = {
126
+ schema_version: 'v1',
127
+ name_for_human: @config.mcp_name_for_human,
128
+ name_for_model: @config.mcp_name,
129
+ description_for_human: @config.mcp_description_for_human,
130
+ description_for_model: @config.mcp_description_for_model,
131
+ auth: {
132
+ type: @config.auth_type
133
+ },
134
+ api: {
135
+ type: 'function'
136
+ },
137
+ logo_url: "#{request.base_url}/logo.png",
138
+ contact_email: 'support@example.com',
139
+ legal_info_url: 'https://example.com/legal'
140
+ }
141
+
142
+ json manifest
143
+ end
144
+
145
+ # MCP endpoint
146
+ post '/mcp' do
147
+ content_type :json
148
+
149
+ # Create server context with user details
150
+ user_details = @config.to_context(request)
151
+
152
+ # Create MCP server with our tools and prompts
153
+ mcp = MCP::Server.new(
154
+ name: @config.mcp_name,
155
+ tools: tool_registry.dynamic_tools,
156
+ prompts: @config.prompts,
157
+ server_context: user_details
158
+ )
159
+
160
+ MCP.configure do |config|
161
+ config.instrumentation_callback = lambda { |data|
162
+ @config.logger.debug "Got instrumentation data #{data.inspect}"
163
+ }
164
+ end
165
+ # Process the MCP request
166
+ request_body = request.body.read
167
+ @config.logger.debug "MCP request: #{request_body}"
168
+
169
+ response_body = mcp.handle_json(request_body)
170
+ @config.logger.debug "MCP response: #{response_body}"
171
+
172
+ response_body
173
+ end
174
+
175
+ # Set auth token endpoint
176
+ post '/set_auth_token' do
177
+ content_type :json
178
+
179
+ begin
180
+ # Parse request body
181
+ request_data = JSON.parse(request.body.read)
182
+ user_id = request_data['user_id'] || request.env['HTTP_X_USER_ID'] || '1'
183
+ auth_token = request_data['auth_token']
184
+
185
+ if auth_token.nil? || auth_token.empty?
186
+ status 400
187
+ return { error: 'Missing auth_token parameter' }.to_json
188
+ end
189
+
190
+ # Set the auth token
191
+ @auth_handler.set_auth_token(user_id, auth_token)
192
+
193
+ # Return success
194
+ {
195
+ success: true,
196
+ message: "Auth token set for user: #{user_id}"
197
+ }.to_json
198
+ rescue StandardError => e
199
+ status 500
200
+ { error: e.message }.to_json
201
+ end
202
+ end
203
+
204
+ # Generate tools endpoint
205
+ post '/generate_tools' do
206
+ content_type :json
207
+
208
+ # Parse request body
209
+ request_data = JSON.parse(request.body.read)
210
+ swagger_url = request_data['swagger_url']
211
+
212
+ if swagger_url.nil? || swagger_url.empty?
213
+ status 400
214
+ return { error: 'Missing swagger_url parameter' }.to_json
215
+ end
216
+
217
+ # Generate tools
218
+ generate_tools_from_swagger_url(swagger_url)
219
+
220
+ # Return success
221
+ {
222
+ success: true,
223
+ message: "Generated #{@dynamic_tools.size} tools",
224
+ tools: @dynamic_tools.map { |t| { name: t.name, description: t.description } }
225
+ }.to_json
226
+ rescue StandardError => e
227
+ status 500
228
+ { error: e.message }.to_json
229
+ end
230
+
231
+ # Logo endpoint
232
+ get '/logo.png' do
233
+ # You can replace this with an actual logo file
234
+ content_type 'image/png'
235
+ File.read('logo.png') if File.exist?('logo.png')
236
+ end
237
+
238
+ # Health check endpoint
239
+ get '/health' do
240
+ content_type :json
241
+ {
242
+ status: 'ok',
243
+ timestamp: Time.now.to_s,
244
+ tools_count: @dynamic_tools.size,
245
+ prompts_count: @config.prompts.size,
246
+ cache_stats: @tool_cache.stats,
247
+ config: {
248
+ server_port: @config.server_port,
249
+ swagger_url: @config.swagger_url
250
+ }
251
+ }.to_json
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'net/http'
5
+ require 'openssl'
6
+ require 'json'
7
+ require 'yaml'
8
+
9
+ module SwaggerMCPTool
10
+ # The SwaggerClient class is responsible for fetching and parsing Swagger/OpenAPI specifications
11
+ # from a given URL. It supports both Swagger 2.0 and OpenAPI 3.x formats, handling JSON and YAML
12
+ # content types. The class provides methods to extract the base URL from the specification and
13
+ # manages HTTP(S) requests with proper SSL configuration and error handling.
14
+ #
15
+ # Example usage:
16
+ # client = SwaggerMCPTool::SwaggerClient.new('https://example.com/swagger.json')
17
+ # spec = client.swagger_spec
18
+ # base_url = client.base_url
19
+ #
20
+ # Attributes:
21
+ # @swagger_spec [Hash] The parsed Swagger/OpenAPI specification.
22
+ # @base_url [String] The base URL extracted from the specification.
23
+ #
24
+ # Methods:
25
+ # - initialize(url): Initializes the client and fetches the specification.
26
+ # - fetch_swagger_spec(url): Fetches and parses the Swagger/OpenAPI spec from the URL.
27
+ # - parse_url_content(url, content): Parses the content as JSON or YAML.
28
+ # - get_base_url(swagger_spec): Determines the base URL from the spec.
29
+ # - get_base_for_swagger_v2(swagger_spec): Extracts base URL for Swagger 2.0.
30
+ # - get_base_for_swagger_v3(swagger_spec): Extracts base URL for OpenAPI 3.x.
31
+ # - make_http_request(url): Performs the HTTP GET request.
32
+ # - create_http_client(uri): Creates and configures the HTTP client.
33
+ # - configure_ssl(http, uri): Configures SSL for HTTPS requests.
34
+ # - validate_response!(response): Validates the HTTP response.
35
+ #
36
+ # @see https://swagger.io/specification/
37
+ class SwaggerClient
38
+ attr_reader :swagger_spec, :base_url
39
+
40
+ def initialize(url)
41
+ @config = Config.instance
42
+ @swagger_url = url
43
+ @swagger_spec = fetch_swagger_spec(url)
44
+ @base_url = get_base_url(@swagger_spec)
45
+ end
46
+
47
+ # Fetch Swagger/OpenAPI specification from URL
48
+ def fetch_swagger_spec(url)
49
+ @config.logger.info "Fetching Swagger specification from: #{url}"
50
+
51
+ # Use HTTPS if specified in the URL
52
+ http_response = make_http_request(url)
53
+ validate_response!(http_response)
54
+
55
+ # Parse JSON or YAML based on content
56
+ parse_url_content(url, http_response.body)
57
+ end
58
+
59
+ def parse_url_content(url, content)
60
+ if url.end_with?('.json') || content.strip.start_with?('{')
61
+ JSON.parse(content)
62
+ else
63
+ YAML.safe_load(content)
64
+ end
65
+ end
66
+
67
+ # Get base URL from Swagger spec
68
+ def get_base_url(swagger_spec)
69
+ if swagger_spec['swagger'] == '2.0'
70
+ get_base_for_swagger_v2(swagger_spec)
71
+ elsif swagger_spec['openapi']&.start_with?('3.')
72
+ get_base_for_swagger_v3(swagger_spec)
73
+ else
74
+ ''
75
+ end
76
+ end
77
+
78
+ def get_base_for_swagger_v2(swagger_spec)
79
+ base_path = swagger_spec['basePath'] || ''
80
+ host = swagger_spec['host'] || ''
81
+ schemes = swagger_spec['schemes'] || ['https']
82
+ "#{schemes.first}://#{host}#{base_path}"
83
+ end
84
+
85
+ def get_base_for_swagger_v3(swagger_spec)
86
+ servers = swagger_spec['servers'] || [{ 'url' => '' }]
87
+ servers.first['url']
88
+ end
89
+
90
+ def make_http_request(url)
91
+ uri = URI(url)
92
+ http = create_http_client(uri)
93
+ request = Net::HTTP::Get.new(uri.request_uri)
94
+ http.request(request)
95
+ end
96
+
97
+ def create_http_client(uri)
98
+ http = Net::HTTP.new(uri.host, uri.port)
99
+ configure_ssl(http, uri)
100
+ http
101
+ end
102
+
103
+ def configure_ssl(http, uri)
104
+ return unless uri.scheme == 'https'
105
+
106
+ http.use_ssl = true
107
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
108
+ end
109
+
110
+ def validate_response!(response)
111
+ return if response.is_a?(Net::HTTPSuccess)
112
+
113
+ raise "Failed to fetch Swagger specification: #{response.code} #{response.message}"
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'config'
4
+ require_relative 'logging'
5
+ require_relative 'helpers/swagger_validator'
6
+ require_relative 'helpers/parameter_processor'
7
+ require_relative 'helpers/request_body_processor'
8
+ require_relative 'helpers/tool_builder'
9
+
10
+ module SwaggerMCPTool
11
+ # Provides validation methods for Swagger (OpenAPI) specification structures.
12
+ # This module includes methods to validate the overall structure of a Swagger spec,
13
+ # ensure required keys are present, and check the structure of the 'paths' section.
14
+ # Intended to be included in classes that require Swagger spec validation logic.
15
+ #
16
+ # Example usage:
17
+ # include SwaggerValidator
18
+ # validate_swagger_spec!(swagger_spec_hash)
19
+ #
20
+ # Raises ArgumentError if the spec or paths are invalid.
21
+ #
22
+ # @see https://swagger.io/specification/
23
+
24
+ # Generate MCP tools from Swagger/OpenAPI specification
25
+ # Generates a list of tool definitions based on the Swagger/OpenAPI specification.
26
+ class ToolGenerator
27
+ include Helpers::SwaggerValidator
28
+ include Helpers::ParameterProcessor
29
+ include Helpers::RequestBodyProcessor
30
+ include Helpers::ToolBuilder
31
+
32
+ def initialize(swagger_client)
33
+ @swagger_client = swagger_client
34
+ @config = Config.instance
35
+ @generated_tools = []
36
+ end
37
+
38
+ def generate_tools
39
+ swagger_spec = @swagger_client.swagger_spec
40
+ validate_swagger_spec!(swagger_spec)
41
+
42
+ @config.logger.info 'Generating tools from Swagger spec'
43
+
44
+ process_swagger_paths(swagger_spec['paths'])
45
+
46
+ @config.logger.info "Generated #{@generated_tools.size} tools"
47
+ @generated_tools
48
+ end
49
+
50
+ private
51
+
52
+ def process_swagger_paths(paths)
53
+ paths.each do |path, methods|
54
+ process_path_methods(path, methods) if methods.is_a?(Hash)
55
+ end
56
+ rescue StandardError => e
57
+ @config.logger.error "Error processing paths: #{e.message}"
58
+ @generated_tools
59
+ end
60
+
61
+ def process_path_methods(path, methods)
62
+ methods.each do |method, operation|
63
+ next if skip_method?(method, operation)
64
+
65
+ tool = build_tool_from_operation(path, method, operation)
66
+ @generated_tools << tool if tool
67
+ end
68
+ end
69
+
70
+ def skip_method?(method, operation)
71
+ method == 'parameters' || !operation.is_a?(Hash)
72
+ end
73
+
74
+ def build_tool_from_operation(path, method, operation)
75
+ tool_definition = create_base_tool_definition(path, method, operation)
76
+
77
+ add_parameters_to_tool(tool_definition, operation)
78
+ add_request_body_to_tool(tool_definition, operation)
79
+
80
+ tool_definition
81
+ rescue StandardError => e
82
+ @config.logger.warn "Skipping tool for #{method.upcase} #{path}: #{e.message}"
83
+ end
84
+ end
85
+ end