From 6944a21763cc0a48507b003ad683d3f0a371c0b0 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Thu, 6 Mar 2025 20:40:24 +0000 Subject: [PATCH 01/13] Add MCP support Signed-off-by: Stan Lo --- .cursor/mcp.json | 7 + .vscode/mcp.json | 9 + Gemfile.lock | 4 + lib/ruby_lsp/global_state.rb | 29 + lib/ruby_lsp/internal.rb | 3 + lib/ruby_lsp/mcp_server.rb | 433 +++ lib/ruby_lsp/server.rb | 11 + ruby-lsp.gemspec | 2 + sorbet/rbi/gems/json_rpc_handler@0.1.1.rbi | 105 + sorbet/rbi/gems/webrick@1.9.1.rbi | 2857 ++++++++++++++++++++ sorbet/tapioca/require.rb | 2 + test/global_state_test.rb | 75 +- test/mcp_server_test.rb | 349 +++ vscode/ruby-mcp-bridge | 41 + vscode/src/workspace.ts | 14 + 15 files changed, 3940 insertions(+), 1 deletion(-) create mode 100644 .cursor/mcp.json create mode 100644 .vscode/mcp.json create mode 100644 lib/ruby_lsp/mcp_server.rb create mode 100644 sorbet/rbi/gems/json_rpc_handler@0.1.1.rbi create mode 100644 sorbet/rbi/gems/webrick@1.9.1.rbi create mode 100644 test/mcp_server_test.rb create mode 100755 vscode/ruby-mcp-bridge diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000000..5b61d19fac --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,7 @@ +{ + "mcpServers": { + "rubyMcp": { + "command": "./.ruby-lsp/ruby-mcp-bridge" + } + } +} diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000000..cc7bedca63 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,9 @@ +{ + "inputs": [], + "servers": { + "rubyMcp": { + "type": "stdio", + "command": "${workspaceFolder}/.ruby-lsp/ruby-mcp-bridge" + } + } +} diff --git a/Gemfile.lock b/Gemfile.lock index 3fcc57a501..66453dcd1c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,10 +2,12 @@ PATH remote: . specs: ruby-lsp (0.23.24) + json_rpc_handler (~> 0.1.1) language_server-protocol (~> 3.17.0) prism (>= 1.2, < 2.0) rbs (>= 3, < 5) sorbet-runtime (>= 0.5.10782) + webrick (>= 1.8) GEM remote: https://rubygems.org/ @@ -23,6 +25,7 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.10.2) + json_rpc_handler (0.1.1) language_server-protocol (3.17.0.4) lint_roller (1.1.0) logger (1.7.0) @@ -117,6 +120,7 @@ GEM unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) + webrick (1.9.1) yard (0.9.37) yard-sorbet (0.9.0) sorbet-runtime diff --git a/lib/ruby_lsp/global_state.rb b/lib/ruby_lsp/global_state.rb index 023d642b26..8ad5f9ada4 100644 --- a/lib/ruby_lsp/global_state.rb +++ b/lib/ruby_lsp/global_state.rb @@ -33,6 +33,9 @@ class GlobalState #: String? attr_reader :telemetry_machine_id + #: bool + attr_reader :uses_ruby_mcp + #: -> void def initialize @workspace_uri = URI::Generic.from_path(path: Dir.pwd) #: URI::Generic @@ -56,6 +59,7 @@ def initialize @enabled_feature_flags = {} #: Hash[Symbol, bool] @mutex = Mutex.new #: Mutex @telemetry_machine_id = nil #: String? + @uses_ruby_mcp = false #: bool end #: [T] { -> T } -> T @@ -151,6 +155,9 @@ def apply_options(options) ) end + @uses_ruby_mcp = detects_ruby_mcp + notifications << Notification.window_log_message("Uses Ruby MCP: #{@uses_ruby_mcp}") + encodings = options.dig(:capabilities, :general, :positionEncodings) @encoding = if !encodings || encodings.empty? Encoding::UTF_16LE @@ -205,8 +212,30 @@ def supports_watching_files @client_capabilities.supports_watching_files end + #: -> bool + def detects_ruby_mcp + check_mcp_file(".vscode/mcp.json", ["servers", "rubyMcp"]) || + check_mcp_file(".cursor/mcp.json", ["mcpServers", "rubyMcp"]) + end + private + # Helper method to check for rubyMcp configuration in a specific file + #: (String relative_path, Array[String] keys_to_check) -> bool + def check_mcp_file(relative_path, keys_to_check) + file_path = File.join(workspace_path, relative_path) + return false unless File.exist?(file_path) + + begin + config = JSON.parse(File.read(file_path)) + # Check if the nested keys exist + !!config.dig(*keys_to_check) + rescue JSON::ParserError + # If JSON parsing fails, consider it not configured + false + end + end + #: (Array[String] direct_dependencies, Array[String] all_dependencies) -> String def detect_formatter(direct_dependencies, all_dependencies) # NOTE: Intentionally no $ at end, since we want to match rubocop-shopify, etc. diff --git a/lib/ruby_lsp/internal.rb b/lib/ruby_lsp/internal.rb index 420ca665e2..9a2087e567 100644 --- a/lib/ruby_lsp/internal.rb +++ b/lib/ruby_lsp/internal.rb @@ -31,6 +31,8 @@ require "securerandom" require "shellwords" require "set" +require "webrick" +require "json_rpc_handler" require "ruby-lsp" require "ruby_lsp/base_server" @@ -41,6 +43,7 @@ require "ruby_lsp/client_capabilities" require "ruby_lsp/global_state" require "ruby_lsp/server" +require "ruby_lsp/mcp_server" require "ruby_lsp/type_inferrer" require "ruby_lsp/node_context" require "ruby_lsp/document" diff --git a/lib/ruby_lsp/mcp_server.rb b/lib/ruby_lsp/mcp_server.rb new file mode 100644 index 0000000000..708b83ac6e --- /dev/null +++ b/lib/ruby_lsp/mcp_server.rb @@ -0,0 +1,433 @@ +# typed: strict +# frozen_string_literal: true + +# TODO: Extract MCP requests and load them separately +require "ruby_lsp/requests/support/common" + +module RubyLsp + class MCPServer + include RubyLsp::Requests::Support::Common + + MAX_CLASSES_TO_RETURN = 5000 + + class << self + # Find an available TCP port + #: -> Integer + def find_available_port + server = TCPServer.new("127.0.0.1", 0) + port = server.addr[1] + server.close + port + end + end + + #: (GlobalState) -> void + def initialize(global_state) + @workspace_path = T.let(global_state.workspace_path, String) + @port = T.let(self.class.find_available_port, Integer) + + # Create .ruby-lsp directory if it doesn't exist + lsp_dir = File.join(@workspace_path, ".ruby-lsp") + FileUtils.mkdir_p(lsp_dir) + + # Write port to file + port_file = File.join(lsp_dir, "mcp-port") + File.write(port_file, @port.to_s) + + # Create WEBrick server + @server = T.let( + WEBrick::HTTPServer.new( + Port: @port, + BindAddress: "127.0.0.1", + Logger: WEBrick::Log.new(File.join(lsp_dir, "mcp-webrick.log")), + AccessLog: [], + ), + WEBrick::HTTPServer, + ) + + # Mount the MCP handler + @server.mount_proc("/mcp") do |req, res| + handle_mcp_request(req, res) + end + + @running = T.let(false, T::Boolean) + @global_state = T.let(global_state, GlobalState) + @index = T.let(global_state.index, RubyIndexer::Index) + end + + #: -> void + def start + puts "[MCP] Server started on TCP port #{@port}" + Thread.new do + @server.start + end + end + + #: -> void + def stop + puts "[MCP] Stopping server" + @server.shutdown + + # Clean up port file + lsp_dir = File.join(@workspace_path, ".ruby-lsp") + port_file = File.join(lsp_dir, "mcp-port") + File.delete(port_file) if File.exist?(port_file) + + # Clean up log file + log_file = File.join(lsp_dir, "mcp-webrick.log") + File.delete(log_file) if File.exist?(log_file) + end + + private + + #: (WEBrick::HTTPRequest, WEBrick::HTTPResponse) -> void + def handle_mcp_request(request, response) + body = request.body || "" + + puts "[MCP] Received request: #{body}" + + result = process_jsonrpc_request(body) + + if result.nil? + response.status = 500 + response.body = { + jsonrpc: "2.0", + id: nil, + error: { + code: JsonRpcHandler::ErrorCode::InternalError, + message: "Internal error", + data: "No response from the server", + }, + }.to_json + else + response.status = 200 + response.content_type = "application/json" + response.body = result + end + + puts "[MCP] Sent response: #{response.body}" + rescue => e + puts "[MCP] Error processing request: #{e.message}" + puts e.backtrace&.join("\n") + + response.status = 500 + response.body = { + jsonrpc: "2.0", + id: nil, + error: { + code: JsonRpcHandler::ErrorCode::InternalError, + message: "Internal error", + data: e.message, + }, + }.to_json + end + + #: (String) -> String? + def process_jsonrpc_request(json) + puts "[MCP] Processing request: #{json.inspect}" + + JsonRpcHandler.handle_json(json) do |method_name| + case method_name + when "initialize" + ->(_) do + { + protocolVersion: "2024-11-05", + capabilities: { + tools: { list_changed: false }, + }, + serverInfo: { + name: "ruby-lsp-mcp-server", + version: "0.1.0", + }, + } + end + when "initialized", "notifications/initialized" + ->(_) do + {} + end + when "tools/list" + ->(_) do + { + tools: [ + { + name: "get_classes_and_modules", + description: <<~DESCRIPTION, + Show all the indexed classes and modules in the current project and its dependencies when no query is provided. + When a query is provided, it'll return a list of classes and modules that match the query. + Doesn't support pagination and will return all classes and modules. + Stops after #{MAX_CLASSES_TO_RETURN} classes and modules. + DESCRIPTION + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "A query to filter the classes and modules", + }, + }, + }, + }, + { + # This may be redundant to some clients if they can access terminal to cat the files + # but it's useful for some clients that don't have that capability + name: "read_ruby_files", + description: <<~DESCRIPTION, + Read the contents of the given Ruby files, including files from dependencies. + DESCRIPTION + inputSchema: { + type: "object", + properties: { + file_uris: { + type: "array", + items: { type: "string" }, + }, + }, + required: ["file_uris"], + }, + }, + { + name: "get_methods_details", + description: <<~DESCRIPTION, + Show the details of the given methods. + Use the following format for the signatures: + - Class#method + - Module#method + - Class.singleton_method + - Module.singleton_method + + Details include: + - Comments + - Definition location + - Visibility + - Parameters + - Owner + DESCRIPTION + inputSchema: { + type: "object", + properties: { + signatures: { + type: "array", + items: { type: "string" }, + }, + }, + required: ["signatures"], + }, + }, + { + name: "get_class_module_details", + description: <<~DESCRIPTION, + Show the details of the given classes/modules that are available in the current project and + its dependencies. + - Comments + - Definition location + - Methods + - Ancestors + + Use `get_methods_details` tool to get the details of specific methods of a class/module. + DESCRIPTION + inputSchema: { + type: "object", + properties: { + fully_qualified_names: { + type: "array", + items: { type: "string" }, + }, + }, + required: ["fully_qualified_names"], + }, + }, + ], + } + end + when "tools/call" + ->(params) { + puts "[MCP] Received tools/call request: #{params.inspect}" + contents = case params[:name] + when "get_classes_and_modules" + handle_get_classes_and_modules(params.dig(:arguments, :query)) + when "read_ruby_files" + handle_read_ruby_files(params.dig(:arguments, :file_uris)) + when "get_methods_details" + handle_get_methods_details(params.dig(:arguments, :signatures)) + when "get_class_module_details" + handle_get_class_module_details(params.dig(:arguments, :fully_qualified_names)) + end + + generate_response(contents) if contents + } + end + end + end + + #: (Array[Hash[Symbol, untyped]]) -> Hash[Symbol, untyped] + def generate_response(contents) + if contents.empty? + { + content: [ + { + type: "text", + text: "No results found", + }, + ], + } + else + { + content: contents, + } + end + end + + # Tool implementations + #: (String?) -> Array[Hash[Symbol, untyped]] + def handle_get_classes_and_modules(query) + class_names = @index.fuzzy_search(query).map do |entry| + case entry + when RubyIndexer::Entry::Class + { + name: entry.name, + type: "class", + } + when RubyIndexer::Entry::Module + { + name: entry.name, + type: "module", + } + end + end.compact.uniq + + if class_names.size > MAX_CLASSES_TO_RETURN + [ + { + type: "text", + text: "Too many classes and modules to return, please narrow down your request with a query.", + }, + { + type: "text", + text: class_names.first(MAX_CLASSES_TO_RETURN).to_yaml, + }, + ] + else + [ + { + type: "text", + text: class_names.to_yaml, + }, + ] + end + end + + #: (Array[String]) -> Array[Hash[Symbol, untyped]] + def handle_read_ruby_files(file_uris) + file_uris.map do |file_uri| + file_uri_obj = URI(file_uri) + file_path = file_uri_obj.path + next unless file_path + + begin + file_content = File.read(file_path) + { + type: "text", + text: { + file_path: file_path, + file_content: file_content, + }.to_yaml, + } + rescue Errno::ENOENT + { + type: "text", + text: { + file_path: file_path, + error: "File not found", + }.to_yaml, + } + rescue => e + { + type: "text", + text: { + file_path: file_path, + error: "Error reading file: #{e.message}", + }.to_yaml, + } + end + end.compact + end + + #: (Array[String]) -> Array[Hash[Symbol, untyped]] + def handle_get_methods_details(signatures) + signatures.map do |signature| + entries = nil + receiver = nil + method = nil + + if signature.include?("#") + receiver, method = signature.split("#") + entries = @index.resolve_method(T.must(method), T.must(receiver)) + elsif signature.include?(".") + receiver, method = signature.split(".") + singleton_class = @index.existing_or_new_singleton_class(T.must(receiver)) + entries = @index.resolve_method(T.must(method), singleton_class.name) + end + + next if entries.nil? + + entry_details = entries.map do |entry| + { + uri: entry.uri, + visibility: entry.visibility, + comments: entry.comments, + parameters: entry.decorated_parameters, + owner: entry.owner&.name, + } + end + + { + type: "text", + text: { + receiver: receiver, + method: method, + entry_details: entry_details, + }.to_yaml, + } + end.compact + end + + #: (Array[String]) -> Array[Hash[Symbol, untyped]] + def handle_get_class_module_details(fully_qualified_names) + fully_qualified_names.map do |fully_qualified_name| + *nestings, name = fully_qualified_name.delete_prefix("::").split("::") + entries = @index.resolve(T.must(name), nestings) || [] + + begin + ancestors = @index.linearized_ancestors_of(fully_qualified_name) + methods = @index.method_completion_candidates(nil, fully_qualified_name) + rescue RubyIndexer::Index::NonExistingNamespaceError + # If the namespace doesn't exist, we can't find ancestors or methods + ancestors = [] + methods = [] + end + + type = case entries.first + when RubyIndexer::Entry::Class + "class" + when RubyIndexer::Entry::Module + "module" + else + "unknown" + end + + { + type: "text", + text: { + name: fully_qualified_name, + nestings: nestings, + type: type, + ancestors: ancestors, + methods: methods.map(&:name), + uris: entries.map(&:uri), + documentation: markdown_from_index_entries(T.must(name), entries), + }.to_yaml, + } + end + end + end +end diff --git a/lib/ruby_lsp/server.rb b/lib/ruby_lsp/server.rb index 6f1838713c..ba6bacbf25 100644 --- a/lib/ruby_lsp/server.rb +++ b/lib/ruby_lsp/server.rb @@ -372,6 +372,16 @@ def run_initialized perform_initial_indexing check_formatter_is_available + + start_mcp_server + end + + #: -> void + def start_mcp_server + return unless @global_state.uses_ruby_mcp + + @mcp_server = T.let(MCPServer.new(@global_state), T.nilable(MCPServer)) + T.must(@mcp_server).start end #: (Hash[Symbol, untyped] message) -> void @@ -1245,6 +1255,7 @@ def workspace_dependencies(message) #: -> void def shutdown Addon.unload_addons + @mcp_server&.stop end #: -> void diff --git a/ruby-lsp.gemspec b/ruby-lsp.gemspec index 3553c47343..53af22395b 100644 --- a/ruby-lsp.gemspec +++ b/ruby-lsp.gemspec @@ -19,10 +19,12 @@ Gem::Specification.new do |s| s.require_paths = ["lib"] # Dependencies must be kept in sync with the checks in the extension side on workspace.ts + s.add_dependency("json_rpc_handler", "~> 0.1.1") s.add_dependency("language_server-protocol", "~> 3.17.0") s.add_dependency("prism", ">= 1.2", "< 2.0") s.add_dependency("rbs", ">= 3", "< 5") s.add_dependency("sorbet-runtime", ">= 0.5.10782") + s.add_dependency("webrick", ">= 1.8") s.required_ruby_version = ">= 3.0" end diff --git a/sorbet/rbi/gems/json_rpc_handler@0.1.1.rbi b/sorbet/rbi/gems/json_rpc_handler@0.1.1.rbi new file mode 100644 index 0000000000..212f4d4c61 --- /dev/null +++ b/sorbet/rbi/gems/json_rpc_handler@0.1.1.rbi @@ -0,0 +1,105 @@ +# typed: true + +# DO NOT EDIT MANUALLY +# This is an autogenerated file for types exported from the `json_rpc_handler` gem. +# Please instead update this file by running `bin/tapioca gem json_rpc_handler`. + + +# source://json_rpc_handler//lib/json_rpc_handler/version.rb#3 +module JsonRpcHandler + private + + # source://json_rpc_handler//lib/json_rpc_handler.rb#137 + def error_response(id:, error:); end + + # source://json_rpc_handler//lib/json_rpc_handler.rb#21 + def handle(request, &method_finder); end + + # source://json_rpc_handler//lib/json_rpc_handler.rb#49 + def handle_json(request_json, &method_finder); end + + # source://json_rpc_handler//lib/json_rpc_handler.rb#64 + def process_request(request, &method_finder); end + + # source://json_rpc_handler//lib/json_rpc_handler.rb#129 + def success_response(id:, result:); end + + # source://json_rpc_handler//lib/json_rpc_handler.rb#117 + def valid_id?(id); end + + # source://json_rpc_handler//lib/json_rpc_handler.rb#121 + def valid_method_name?(method); end + + # source://json_rpc_handler//lib/json_rpc_handler.rb#125 + def valid_params?(params); end + + # source://json_rpc_handler//lib/json_rpc_handler.rb#113 + def valid_version?(version); end + + class << self + # source://json_rpc_handler//lib/json_rpc_handler.rb#137 + def error_response(id:, error:); end + + # source://json_rpc_handler//lib/json_rpc_handler.rb#21 + def handle(request, &method_finder); end + + # source://json_rpc_handler//lib/json_rpc_handler.rb#49 + def handle_json(request_json, &method_finder); end + + # source://json_rpc_handler//lib/json_rpc_handler.rb#64 + def process_request(request, &method_finder); end + + # source://json_rpc_handler//lib/json_rpc_handler.rb#129 + def success_response(id:, result:); end + + # @return [Boolean] + # + # source://json_rpc_handler//lib/json_rpc_handler.rb#117 + def valid_id?(id); end + + # @return [Boolean] + # + # source://json_rpc_handler//lib/json_rpc_handler.rb#121 + def valid_method_name?(method); end + + # @return [Boolean] + # + # source://json_rpc_handler//lib/json_rpc_handler.rb#125 + def valid_params?(params); end + + # @return [Boolean] + # + # source://json_rpc_handler//lib/json_rpc_handler.rb#113 + def valid_version?(version); end + end +end + +# source://json_rpc_handler//lib/json_rpc_handler.rb#11 +class JsonRpcHandler::ErrorCode; end + +# source://json_rpc_handler//lib/json_rpc_handler.rb#15 +JsonRpcHandler::ErrorCode::InternalError = T.let(T.unsafe(nil), Integer) + +# source://json_rpc_handler//lib/json_rpc_handler.rb#14 +JsonRpcHandler::ErrorCode::InvalidParams = T.let(T.unsafe(nil), Integer) + +# source://json_rpc_handler//lib/json_rpc_handler.rb#12 +JsonRpcHandler::ErrorCode::InvalidRequest = T.let(T.unsafe(nil), Integer) + +# source://json_rpc_handler//lib/json_rpc_handler.rb#13 +JsonRpcHandler::ErrorCode::MethodNotFound = T.let(T.unsafe(nil), Integer) + +# source://json_rpc_handler//lib/json_rpc_handler.rb#16 +JsonRpcHandler::ErrorCode::ParseError = T.let(T.unsafe(nil), Integer) + +# source://json_rpc_handler//lib/json_rpc_handler/version.rb#4 +JsonRpcHandler::VERSION = T.let(T.unsafe(nil), String) + +# source://json_rpc_handler//lib/json_rpc_handler.rb#6 +class JsonRpcHandler::Version; end + +# source://json_rpc_handler//lib/json_rpc_handler.rb#7 +JsonRpcHandler::Version::V1_0 = T.let(T.unsafe(nil), String) + +# source://json_rpc_handler//lib/json_rpc_handler.rb#8 +JsonRpcHandler::Version::V2_0 = T.let(T.unsafe(nil), String) diff --git a/sorbet/rbi/gems/webrick@1.9.1.rbi b/sorbet/rbi/gems/webrick@1.9.1.rbi new file mode 100644 index 0000000000..a7a6e174fb --- /dev/null +++ b/sorbet/rbi/gems/webrick@1.9.1.rbi @@ -0,0 +1,2857 @@ +# typed: true + +# DO NOT EDIT MANUALLY +# This is an autogenerated file for types exported from the `webrick` gem. +# Please instead update this file by running `bin/tapioca gem webrick`. + + +# AccessLog provides logging to various files in various formats. +# +# Multiple logs may be written to at the same time: +# +# access_log = [ +# [$stderr, WEBrick::AccessLog::COMMON_LOG_FORMAT], +# [$stderr, WEBrick::AccessLog::REFERER_LOG_FORMAT], +# ] +# +# server = WEBrick::HTTPServer.new :AccessLog => access_log +# +# Custom log formats may be defined. WEBrick::AccessLog provides a subset +# of the formatting from Apache's mod_log_config +# http://httpd.apache.org/docs/mod/mod_log_config.html#formats. See +# AccessLog::setup_params for a list of supported options +# +# source://webrick//lib/webrick/accesslog.rb#30 +module WEBrick::AccessLog + private + + # Escapes control characters in +data+ + # + # source://webrick//lib/webrick/accesslog.rb#151 + def escape(data); end + + # Formats +params+ according to +format_string+ which is described in + # setup_params. + # + # source://webrick//lib/webrick/accesslog.rb#123 + def format(format_string, params); end + + # This format specification is a subset of mod_log_config of Apache: + # + # %a:: Remote IP address + # %b:: Total response size + # %e{variable}:: Given variable in ENV + # %f:: Response filename + # %h:: Remote host name + # %{header}i:: Given request header + # %l:: Remote logname, always "-" + # %m:: Request method + # %{attr}n:: Given request attribute from req.attributes + # %{header}o:: Given response header + # %p:: Server's request port + # %{format}p:: The canonical port of the server serving the request or the + # actual port or the client's actual port. Valid formats are + # canonical, local or remote. + # %q:: Request query string + # %r:: First line of the request + # %s:: Request status + # %t:: Time the request was received + # %T:: Time taken to process the request + # %u:: Remote user from auth + # %U:: Unparsed URI + # %%:: Literal % + # + # source://webrick//lib/webrick/accesslog.rb#95 + def setup_params(config, req, res); end + + class << self + # Escapes control characters in +data+ + # + # source://webrick//lib/webrick/accesslog.rb#151 + def escape(data); end + + # Formats +params+ according to +format_string+ which is described in + # setup_params. + # + # source://webrick//lib/webrick/accesslog.rb#123 + def format(format_string, params); end + + # This format specification is a subset of mod_log_config of Apache: + # + # %a:: Remote IP address + # %b:: Total response size + # %e{variable}:: Given variable in ENV + # %f:: Response filename + # %h:: Remote host name + # %{header}i:: Given request header + # %l:: Remote logname, always "-" + # %m:: Request method + # %{attr}n:: Given request attribute from req.attributes + # %{header}o:: Given response header + # %p:: Server's request port + # %{format}p:: The canonical port of the server serving the request or the + # actual port or the client's actual port. Valid formats are + # canonical, local or remote. + # %q:: Request query string + # %r:: First line of the request + # %s:: Request status + # %t:: Time the request was received + # %T:: Time taken to process the request + # %u:: Remote user from auth + # %U:: Unparsed URI + # %%:: Literal % + # + # source://webrick//lib/webrick/accesslog.rb#95 + def setup_params(config, req, res); end + end +end + +# A generic logging class +# +# source://webrick//lib/webrick/log.rb#17 +class WEBrick::BasicLog + # Initializes a new logger for +log_file+ that outputs messages at +level+ + # or higher. +log_file+ can be a filename, an IO-like object that + # responds to #<< or nil which outputs to $stderr. + # + # If no level is given INFO is chosen by default + # + # @return [BasicLog] a new instance of BasicLog + # + # source://webrick//lib/webrick/log.rb#50 + def initialize(log_file = T.unsafe(nil), level = T.unsafe(nil)); end + + # Synonym for log(INFO, obj.to_s) + # + # source://webrick//lib/webrick/log.rb#84 + def <<(obj); end + + # Closes the logger (also closes the log device associated to the logger) + # + # source://webrick//lib/webrick/log.rb#66 + def close; end + + # Shortcut for logging a DEBUG message + # + # source://webrick//lib/webrick/log.rb#97 + def debug(msg); end + + # Will the logger output DEBUG messages? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/log.rb#108 + def debug?; end + + # Shortcut for logging an ERROR message + # + # source://webrick//lib/webrick/log.rb#91 + def error(msg); end + + # Will the logger output ERROR messages? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/log.rb#102 + def error?; end + + # Shortcut for logging a FATAL message + # + # source://webrick//lib/webrick/log.rb#89 + def fatal(msg); end + + # Will the logger output FATAL messages? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/log.rb#100 + def fatal?; end + + # Shortcut for logging an INFO message + # + # source://webrick//lib/webrick/log.rb#95 + def info(msg); end + + # Will the logger output INFO messages? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/log.rb#106 + def info?; end + + # log-level, messages above this level will be logged + # + # source://webrick//lib/webrick/log.rb#41 + def level; end + + # log-level, messages above this level will be logged + # + # source://webrick//lib/webrick/log.rb#41 + def level=(_arg0); end + + # Logs +data+ at +level+ if the given level is above the current log + # level. + # + # source://webrick//lib/webrick/log.rb#75 + def log(level, data); end + + # Shortcut for logging a WARN message + # + # source://webrick//lib/webrick/log.rb#93 + def warn(msg); end + + # Will the logger output WARN messages? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/log.rb#104 + def warn?; end + + private + + # Formats +arg+ for the logger + # + # * If +arg+ is an Exception, it will format the error message and + # the back trace. + # * If +arg+ responds to #to_str, it will return it. + # * Otherwise it will return +arg+.inspect. + # + # source://webrick//lib/webrick/log.rb#119 + def format(arg); end +end + +# Processes HTTP cookies +# +# source://webrick//lib/webrick/cookie.rb#20 +class WEBrick::Cookie + # Creates a new cookie with the given +name+ and +value+ + # + # @return [Cookie] a new instance of Cookie + # + # source://webrick//lib/webrick/cookie.rb#66 + def initialize(name, value); end + + # The cookie comment + # + # source://webrick//lib/webrick/cookie.rb#54 + def comment; end + + # The cookie comment + # + # source://webrick//lib/webrick/cookie.rb#54 + def comment=(_arg0); end + + # The cookie domain + # + # source://webrick//lib/webrick/cookie.rb#39 + def domain; end + + # The cookie domain + # + # source://webrick//lib/webrick/cookie.rb#39 + def domain=(_arg0); end + + # Retrieves the expiration time as a Time + # + # source://webrick//lib/webrick/cookie.rb#87 + def expires; end + + # Sets the cookie expiration to the time +t+. The expiration time may be + # a false value to disable expiration or a Time or HTTP format time string + # to set the expiration date. + # + # source://webrick//lib/webrick/cookie.rb#80 + def expires=(t); end + + # The maximum age of the cookie + # + # source://webrick//lib/webrick/cookie.rb#59 + def max_age; end + + # The maximum age of the cookie + # + # source://webrick//lib/webrick/cookie.rb#59 + def max_age=(_arg0); end + + # The cookie name + # + # source://webrick//lib/webrick/cookie.rb#25 + def name; end + + # The cookie path + # + # source://webrick//lib/webrick/cookie.rb#44 + def path; end + + # The cookie path + # + # source://webrick//lib/webrick/cookie.rb#44 + def path=(_arg0); end + + # Is this a secure cookie? + # + # source://webrick//lib/webrick/cookie.rb#49 + def secure; end + + # Is this a secure cookie? + # + # source://webrick//lib/webrick/cookie.rb#49 + def secure=(_arg0); end + + # The cookie string suitable for use in an HTTP header + # + # source://webrick//lib/webrick/cookie.rb#94 + def to_s; end + + # The cookie value + # + # source://webrick//lib/webrick/cookie.rb#30 + def value; end + + # The cookie value + # + # source://webrick//lib/webrick/cookie.rb#30 + def value=(_arg0); end + + # The cookie version + # + # source://webrick//lib/webrick/cookie.rb#35 + def version; end + + # The cookie version + # + # source://webrick//lib/webrick/cookie.rb#35 + def version=(_arg0); end + + class << self + # Parses a Cookie field sent from the user-agent. Returns an array of + # cookies. + # + # source://webrick//lib/webrick/cookie.rb#111 + def parse(str); end + + # Parses the cookie in +str+ + # + # source://webrick//lib/webrick/cookie.rb#138 + def parse_set_cookie(str); end + + # Parses the cookies in +str+ + # + # source://webrick//lib/webrick/cookie.rb#166 + def parse_set_cookies(str); end + end +end + +# A generic module for daemonizing a process +# +# source://webrick//lib/webrick/server.rb#39 +class WEBrick::Daemon + class << self + # Performs the standard operations for daemonizing a process. Runs a + # block, if given. + # + # source://webrick//lib/webrick/server.rb#45 + def start; end + end +end + +# -- +# Updates WEBrick::GenericServer with SSL functionality +# +# source://webrick//lib/webrick/server.rb#56 +class WEBrick::GenericServer + # Creates a new generic server from +config+. The default configuration + # comes from +default+. + # + # @return [GenericServer] a new instance of GenericServer + # + # source://webrick//lib/webrick/server.rb#88 + def initialize(config = T.unsafe(nil), default = T.unsafe(nil)); end + + # Retrieves +key+ from the configuration + # + # source://webrick//lib/webrick/server.rb#121 + def [](key); end + + # The server configuration + # + # source://webrick//lib/webrick/server.rb#66 + def config; end + + # Updates +listen+ to enable SSL when the SSL configuration is active. + # + # source://webrick//lib/webrick/server.rb#129 + def listen(address, port); end + + # Sockets listening for connections. + # + # source://webrick//lib/webrick/server.rb#82 + def listeners; end + + # The server logger. This is independent from the HTTP access log. + # + # source://webrick//lib/webrick/server.rb#71 + def logger; end + + # You must subclass GenericServer and implement \#run which accepts a TCP + # client socket + # + # source://webrick//lib/webrick/server.rb#244 + def run(sock); end + + # Shuts down the server and all listening sockets. New listeners must be + # provided to restart the server. + # + # source://webrick//lib/webrick/server.rb#234 + def shutdown; end + + # Starts the server and runs the +block+ for each connection. This method + # does not return until the server is stopped from a signal handler or + # another thread using #stop or #shutdown. + # + # If the block raises a subclass of StandardError the exception is logged + # and ignored. If an IOError or Errno::EBADF exception is raised the + # exception is ignored. If an Exception subclass is raised the exception + # is logged and re-raised which stops the server. + # + # To completely shut down a server call #shutdown from ensure: + # + # server = WEBrick::GenericServer.new + # # or WEBrick::HTTPServer.new + # + # begin + # server.start + # ensure + # server.shutdown + # end + # + # @raise [ServerError] + # + # source://webrick//lib/webrick/server.rb#154 + def start(&block); end + + # The server status. One of :Stop, :Running or :Shutdown + # + # source://webrick//lib/webrick/server.rb#61 + def status; end + + # Stops the server from accepting new connections. + # + # source://webrick//lib/webrick/server.rb#222 + def stop; end + + # Tokens control the number of outstanding clients. The + # :MaxClients configuration sets this. + # + # source://webrick//lib/webrick/server.rb#77 + def tokens; end + + private + + # Accepts a TCP client socket from the TCP server socket +svr+ and returns + # the client socket. + # + # source://webrick//lib/webrick/server.rb#256 + def accept_client(svr); end + + # source://webrick//lib/webrick/server.rb#347 + def alarm_shutdown_pipe; end + + # Calls the callback +callback_name+ from the configuration with +args+ + # + # source://webrick//lib/webrick/server.rb#334 + def call_callback(callback_name, *args); end + + # source://webrick//lib/webrick/server.rb#359 + def cleanup_listener; end + + # source://webrick//lib/webrick/server.rb#342 + def cleanup_shutdown_pipe(shutdown_pipe); end + + # source://webrick//lib/webrick/server.rb#338 + def setup_shutdown_pipe; end + + # Starts a server thread for the client socket +sock+ that runs the given + # +block+. + # + # Sets the socket to the :WEBrickSocket thread local variable + # in the thread. + # + # If any errors occur in the block they are logged and handled. + # + # source://webrick//lib/webrick/server.rb#288 + def start_thread(sock, &block); end +end + +# source://webrick//lib/webrick/htmlutils.rb#13 +module WEBrick::HTMLUtils + private + + # Escapes &, ", > and < in +string+ + # + # source://webrick//lib/webrick/htmlutils.rb#18 + def escape(string); end + + class << self + # Escapes &, ", > and < in +string+ + # + # source://webrick//lib/webrick/htmlutils.rb#18 + def escape(string); end + end +end + +# HTTPAuth provides both basic and digest authentication. +# +# To enable authentication for requests in WEBrick you will need a user +# database and an authenticator. To start, here's an Htpasswd database for +# use with a DigestAuth authenticator: +# +# config = { :Realm => 'DigestAuth example realm' } +# +# htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file' +# htpasswd.auth_type = WEBrick::HTTPAuth::DigestAuth +# htpasswd.set_passwd config[:Realm], 'username', 'password' +# htpasswd.flush +# +# The +:Realm+ is used to provide different access to different groups +# across several resources on a server. Typically you'll need only one +# realm for a server. +# +# This database can be used to create an authenticator: +# +# config[:UserDB] = htpasswd +# +# digest_auth = WEBrick::HTTPAuth::DigestAuth.new config +# +# To authenticate a request call #authenticate with a request and response +# object in a servlet: +# +# def do_GET req, res +# @authenticator.authenticate req, res +# end +# +# For digest authentication the authenticator must not be created every +# request, it must be passed in as an option via WEBrick::HTTPServer#mount. +# +# source://webrick//lib/webrick/httpauth/authenticator.rb#12 +module WEBrick::HTTPAuth + private + + # source://webrick//lib/webrick/httpauth.rb#57 + def _basic_auth(req, res, realm, req_field, res_field, err_type, block); end + + # Simple wrapper for providing basic authentication for a request. When + # called with a request +req+, response +res+, authentication +realm+ and + # +block+ the block will be called with a +username+ and +password+. If + # the block returns true the request is allowed to continue, otherwise an + # HTTPStatus::Unauthorized error is raised. + # + # source://webrick//lib/webrick/httpauth.rb#79 + def basic_auth(req, res, realm, &block); end + + # Simple wrapper for providing basic authentication for a proxied request. + # When called with a request +req+, response +res+, authentication +realm+ + # and +block+ the block will be called with a +username+ and +password+. + # If the block returns true the request is allowed to continue, otherwise + # an HTTPStatus::ProxyAuthenticationRequired error is raised. + # + # source://webrick//lib/webrick/httpauth.rb#91 + def proxy_basic_auth(req, res, realm, &block); end + + class << self + # source://webrick//lib/webrick/httpauth.rb#57 + def _basic_auth(req, res, realm, req_field, res_field, err_type, block); end + + # Simple wrapper for providing basic authentication for a request. When + # called with a request +req+, response +res+, authentication +realm+ and + # +block+ the block will be called with a +username+ and +password+. If + # the block returns true the request is allowed to continue, otherwise an + # HTTPStatus::Unauthorized error is raised. + # + # source://webrick//lib/webrick/httpauth.rb#79 + def basic_auth(req, res, realm, &block); end + + # Simple wrapper for providing basic authentication for a proxied request. + # When called with a request +req+, response +res+, authentication +realm+ + # and +block+ the block will be called with a +username+ and +password+. + # If the block returns true the request is allowed to continue, otherwise + # an HTTPStatus::ProxyAuthenticationRequired error is raised. + # + # source://webrick//lib/webrick/httpauth.rb#91 + def proxy_basic_auth(req, res, realm, &block); end + end +end + +# Module providing generic support for both Digest and Basic +# authentication schemes. +# +# source://webrick//lib/webrick/httpauth/authenticator.rb#18 +module WEBrick::HTTPAuth::Authenticator + # The logger for this authenticator + # + # source://webrick//lib/webrick/httpauth/authenticator.rb#43 + def logger; end + + # The realm this authenticator covers + # + # source://webrick//lib/webrick/httpauth/authenticator.rb#33 + def realm; end + + # The user database for this authenticator + # + # source://webrick//lib/webrick/httpauth/authenticator.rb#38 + def userdb; end + + private + + # Initializes the authenticator from +config+ + # + # source://webrick//lib/webrick/httpauth/authenticator.rb#52 + def check_init(config); end + + # Ensures +req+ has credentials that can be authenticated. + # + # source://webrick//lib/webrick/httpauth/authenticator.rb#72 + def check_scheme(req); end + + # source://webrick//lib/webrick/httpauth/authenticator.rb#91 + def error(fmt, *args); end + + # source://webrick//lib/webrick/httpauth/authenticator.rb#97 + def info(fmt, *args); end + + # source://webrick//lib/webrick/httpauth/authenticator.rb#85 + def log(meth, fmt, *args); end +end + +# source://webrick//lib/webrick/httpauth/authenticator.rb#23 +WEBrick::HTTPAuth::Authenticator::AuthException = WEBrick::HTTPStatus::Unauthorized + +# Basic Authentication for WEBrick +# +# Use this class to add basic authentication to a WEBrick servlet. +# +# Here is an example of how to set up a BasicAuth: +# +# config = { :Realm => 'BasicAuth example realm' } +# +# htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file', password_hash: :bcrypt +# htpasswd.set_passwd config[:Realm], 'username', 'password' +# htpasswd.flush +# +# config[:UserDB] = htpasswd +# +# basic_auth = WEBrick::HTTPAuth::BasicAuth.new config +# +# source://webrick//lib/webrick/httpauth/basicauth.rb#35 +class WEBrick::HTTPAuth::BasicAuth + include ::WEBrick::HTTPAuth::Authenticator + + # Creates a new BasicAuth instance. + # + # See WEBrick::Config::BasicAuth for default configuration entries + # + # You must supply the following configuration entries: + # + # :Realm:: The name of the realm being protected. + # :UserDB:: A database of usernames and passwords. + # A WEBrick::HTTPAuth::Htpasswd instance should be used. + # + # @return [BasicAuth] a new instance of BasicAuth + # + # source://webrick//lib/webrick/httpauth/basicauth.rb#61 + def initialize(config, default = T.unsafe(nil)); end + + # Authenticates a +req+ and returns a 401 Unauthorized using +res+ if + # the authentication was not correct. + # + # source://webrick//lib/webrick/httpauth/basicauth.rb#70 + def authenticate(req, res); end + + # Returns a challenge response which asks for authentication information + # + # @raise [@auth_exception] + # + # source://webrick//lib/webrick/httpauth/basicauth.rb#103 + def challenge(req, res); end + + # Returns the value of attribute logger. + # + # source://webrick//lib/webrick/httpauth/basicauth.rb#48 + def logger; end + + # Returns the value of attribute realm. + # + # source://webrick//lib/webrick/httpauth/basicauth.rb#48 + def realm; end + + # Returns the value of attribute userdb. + # + # source://webrick//lib/webrick/httpauth/basicauth.rb#48 + def userdb; end + + class << self + # Used by UserDB to create a basic password entry + # + # source://webrick//lib/webrick/httpauth/basicauth.rb#43 + def make_passwd(realm, user, pass); end + end +end + +# RFC 2617 Digest Access Authentication for WEBrick +# +# Use this class to add digest authentication to a WEBrick servlet. +# +# Here is an example of how to set up DigestAuth: +# +# config = { :Realm => 'DigestAuth example realm' } +# +# htdigest = WEBrick::HTTPAuth::Htdigest.new 'my_password_file' +# htdigest.set_passwd config[:Realm], 'username', 'password' +# htdigest.flush +# +# config[:UserDB] = htdigest +# +# digest_auth = WEBrick::HTTPAuth::DigestAuth.new config +# +# When using this as with a servlet be sure not to create a new DigestAuth +# object in the servlet's #initialize. By default WEBrick creates a new +# servlet instance for every request and the DigestAuth object must be +# used across requests. +# +# source://webrick//lib/webrick/httpauth/digestauth.rb#46 +class WEBrick::HTTPAuth::DigestAuth + include ::WEBrick::HTTPAuth::Authenticator + + # Creates a new DigestAuth instance. Be sure to use the same DigestAuth + # instance for multiple requests as it saves state between requests in + # order to perform authentication. + # + # See WEBrick::Config::DigestAuth for default configuration entries + # + # You must supply the following configuration entries: + # + # :Realm:: The name of the realm being protected. + # :UserDB:: A database of usernames and passwords. + # A WEBrick::HTTPAuth::Htdigest instance should be used. + # + # @return [DigestAuth] a new instance of DigestAuth + # + # source://webrick//lib/webrick/httpauth/digestauth.rb#87 + def initialize(config, default = T.unsafe(nil)); end + + # Digest authentication algorithm + # + # source://webrick//lib/webrick/httpauth/digestauth.rb#59 + def algorithm; end + + # Authenticates a +req+ and returns a 401 Unauthorized using +res+ if + # the authentication was not correct. + # + # source://webrick//lib/webrick/httpauth/digestauth.rb#121 + def authenticate(req, res); end + + # Returns a challenge response which asks for authentication information + # + # @raise [@auth_exception] + # + # source://webrick//lib/webrick/httpauth/digestauth.rb#134 + def challenge(req, res, stale = T.unsafe(nil)); end + + # Quality of protection. RFC 2617 defines "auth" and "auth-int" + # + # source://webrick//lib/webrick/httpauth/digestauth.rb#64 + def qop; end + + private + + # source://webrick//lib/webrick/httpauth/digestauth.rb#163 + def _authenticate(req, res); end + + # source://webrick//lib/webrick/httpauth/digestauth.rb#306 + def check_nonce(req, auth_req); end + + # source://webrick//lib/webrick/httpauth/digestauth.rb#349 + def check_opaque(opaque_struct, req, auth_req); end + + # source://webrick//lib/webrick/httpauth/digestauth.rb#365 + def check_uri(req, auth_req); end + + # source://webrick//lib/webrick/httpauth/digestauth.rb#299 + def generate_next_nonce(req); end + + # source://webrick//lib/webrick/httpauth/digestauth.rb#332 + def generate_opaque(req); end + + # source://webrick//lib/webrick/httpauth/digestauth.rb#376 + def hexdigest(*args); end + + # source://webrick//lib/webrick/httpauth/digestauth.rb#291 + def split_param_value(string); end + + class << self + # Used by UserDB to create a digest password entry + # + # source://webrick//lib/webrick/httpauth/digestauth.rb#69 + def make_passwd(realm, user, pass); end + end +end + +# Htdigest accesses apache-compatible digest password files. Passwords are +# matched to a realm where they are valid. For security, the path for a +# digest password database should be stored outside of the paths available +# to the HTTP server. +# +# Htdigest is intended for use with WEBrick::HTTPAuth::DigestAuth and +# stores passwords using cryptographic hashes. +# +# htpasswd = WEBrick::HTTPAuth::Htdigest.new 'my_password_file' +# htpasswd.set_passwd 'my realm', 'username', 'password' +# htpasswd.flush +# +# source://webrick//lib/webrick/httpauth/htdigest.rb#31 +class WEBrick::HTTPAuth::Htdigest + include ::WEBrick::HTTPAuth::UserDB + + # Open a digest password database at +path+ + # + # @return [Htdigest] a new instance of Htdigest + # + # source://webrick//lib/webrick/httpauth/htdigest.rb#37 + def initialize(path); end + + # Removes a password from the database for +user+ in +realm+. + # + # source://webrick//lib/webrick/httpauth/htdigest.rb#113 + def delete_passwd(realm, user); end + + # Iterate passwords in the database. + # + # source://webrick//lib/webrick/httpauth/htdigest.rb#122 + def each; end + + # Flush the password database. If +output+ is given the database will + # be written there instead of to the original path. + # + # source://webrick//lib/webrick/httpauth/htdigest.rb#72 + def flush(output = T.unsafe(nil)); end + + # Retrieves a password from the database for +user+ in +realm+. If + # +reload_db+ is true the database will be reloaded first. + # + # source://webrick//lib/webrick/httpauth/htdigest.rb#91 + def get_passwd(realm, user, reload_db); end + + # Reloads passwords from the database + # + # source://webrick//lib/webrick/httpauth/htdigest.rb#50 + def reload; end + + # Sets a password in the database for +user+ in +realm+ to +pass+. + # + # source://webrick//lib/webrick/httpauth/htdigest.rb#101 + def set_passwd(realm, user, pass); end +end + +# Htgroup accesses apache-compatible group files. Htgroup can be used to +# provide group-based authentication for users. Currently Htgroup is not +# directly integrated with any authenticators in WEBrick. For security, +# the path for a digest password database should be stored outside of the +# paths available to the HTTP server. +# +# Example: +# +# htgroup = WEBrick::HTTPAuth::Htgroup.new 'my_group_file' +# htgroup.add 'superheroes', %w[spiderman batman] +# +# htgroup.members('superheroes').include? 'magneto' # => false +# +# source://webrick//lib/webrick/httpauth/htgroup.rb#30 +class WEBrick::HTTPAuth::Htgroup + # Open a group database at +path+ + # + # @return [Htgroup] a new instance of Htgroup + # + # source://webrick//lib/webrick/httpauth/htgroup.rb#35 + def initialize(path); end + + # Add an Array of +members+ to +group+ + # + # source://webrick//lib/webrick/httpauth/htgroup.rb#92 + def add(group, members); end + + # Flush the group database. If +output+ is given the database will be + # written there instead of to the original path. + # + # source://webrick//lib/webrick/httpauth/htgroup.rb#64 + def flush(output = T.unsafe(nil)); end + + # Retrieve the list of members from +group+ + # + # source://webrick//lib/webrick/httpauth/htgroup.rb#84 + def members(group); end + + # Reload groups from the database + # + # source://webrick//lib/webrick/httpauth/htgroup.rb#46 + def reload; end +end + +# Htpasswd accesses apache-compatible password files. Passwords are +# matched to a realm where they are valid. For security, the path for a +# password database should be stored outside of the paths available to the +# HTTP server. +# +# Htpasswd is intended for use with WEBrick::HTTPAuth::BasicAuth. +# +# To create an Htpasswd database with a single user: +# +# htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file' +# htpasswd.set_passwd 'my realm', 'username', 'password' +# htpasswd.flush +# +# source://webrick//lib/webrick/httpauth/htpasswd.rb#32 +class WEBrick::HTTPAuth::Htpasswd + include ::WEBrick::HTTPAuth::UserDB + + # Open a password database at +path+ + # + # @return [Htpasswd] a new instance of Htpasswd + # + # source://webrick//lib/webrick/httpauth/htpasswd.rb#38 + def initialize(path, password_hash: T.unsafe(nil)); end + + # Removes a password from the database for +user+ in +realm+. + # + # source://webrick//lib/webrick/httpauth/htpasswd.rb#144 + def delete_passwd(realm, user); end + + # Iterate passwords in the database. + # + # source://webrick//lib/webrick/httpauth/htpasswd.rb#151 + def each; end + + # Flush the password database. If +output+ is given the database will + # be written there instead of to the original path. + # + # source://webrick//lib/webrick/httpauth/htpasswd.rb#103 + def flush(output = T.unsafe(nil)); end + + # Retrieves a password from the database for +user+ in +realm+. If + # +reload_db+ is true the database will be reloaded first. + # + # source://webrick//lib/webrick/httpauth/htpasswd.rb#122 + def get_passwd(realm, user, reload_db); end + + # Reload passwords from the database + # + # source://webrick//lib/webrick/httpauth/htpasswd.rb#68 + def reload; end + + # Sets a password in the database for +user+ in +realm+ to +pass+. + # + # source://webrick//lib/webrick/httpauth/htpasswd.rb#130 + def set_passwd(realm, user, pass); end +end + +# source://webrick//lib/webrick/httpauth/authenticator.rb#114 +WEBrick::HTTPAuth::ProxyAuthenticator::AuthException = WEBrick::HTTPStatus::ProxyAuthenticationRequired + +# source://webrick//lib/webrick/httpauth/authenticator.rb#113 +WEBrick::HTTPAuth::ProxyAuthenticator::ResponseInfoField = T.let(T.unsafe(nil), String) + +# Basic authentication for proxy servers. See BasicAuth for details. +# +# source://webrick//lib/webrick/httpauth/basicauth.rb#112 +class WEBrick::HTTPAuth::ProxyBasicAuth < ::WEBrick::HTTPAuth::BasicAuth + include ::WEBrick::HTTPAuth::ProxyAuthenticator +end + +# Digest authentication for proxy servers. See DigestAuth for details. +# +# source://webrick//lib/webrick/httpauth/digestauth.rb#386 +class WEBrick::HTTPAuth::ProxyDigestAuth < ::WEBrick::HTTPAuth::DigestAuth + include ::WEBrick::HTTPAuth::ProxyAuthenticator + + private + + # source://webrick//lib/webrick/httpauth/digestauth.rb#390 + def check_uri(req, auth_req); end +end + +# User database mixin for HTTPAuth. This mixin dispatches user record +# access to the underlying auth_type for this database. +# +# source://webrick//lib/webrick/httpauth/userdb.rb#18 +module WEBrick::HTTPAuth::UserDB + # The authentication type. + # + # WEBrick::HTTPAuth::BasicAuth or WEBrick::HTTPAuth::DigestAuth are + # built-in. + # + # source://webrick//lib/webrick/httpauth/userdb.rb#26 + def auth_type; end + + # The authentication type. + # + # WEBrick::HTTPAuth::BasicAuth or WEBrick::HTTPAuth::DigestAuth are + # built-in. + # + # source://webrick//lib/webrick/httpauth/userdb.rb#26 + def auth_type=(_arg0); end + + # Retrieves a password in +realm+ for +user+ for the auth_type of this + # database. +reload_db+ is a dummy value. + # + # source://webrick//lib/webrick/httpauth/userdb.rb#48 + def get_passwd(realm, user, reload_db = T.unsafe(nil)); end + + # Creates an obscured password in +realm+ with +user+ and +password+ + # using the auth_type of this database. + # + # source://webrick//lib/webrick/httpauth/userdb.rb#32 + def make_passwd(realm, user, pass); end + + # Sets a password in +realm+ with +user+ and +password+ for the + # auth_type of this database. + # + # source://webrick//lib/webrick/httpauth/userdb.rb#40 + def set_passwd(realm, user, pass); end +end + +# -- +# Adds SSL functionality to WEBrick::HTTPRequest +# +# source://webrick//lib/webrick/httprequest.rb#25 +class WEBrick::HTTPRequest + # Creates a new HTTP request. WEBrick::Config::HTTP is the default + # configuration. + # + # @return [HTTPRequest] a new instance of HTTPRequest + # + # source://webrick//lib/webrick/httprequest.rb#153 + def initialize(config); end + + # Retrieves +header_name+ + # + # source://webrick//lib/webrick/httprequest.rb#318 + def [](header_name); end + + # The Accept header value + # + # source://webrick//lib/webrick/httprequest.rb#100 + def accept; end + + # The Accept-Charset header value + # + # source://webrick//lib/webrick/httprequest.rb#105 + def accept_charset; end + + # The Accept-Encoding header value + # + # source://webrick//lib/webrick/httprequest.rb#110 + def accept_encoding; end + + # The Accept-Language header value + # + # source://webrick//lib/webrick/httprequest.rb#115 + def accept_language; end + + # The socket address of the server + # + # source://webrick//lib/webrick/httprequest.rb#127 + def addr; end + + # Hash of request attributes + # + # source://webrick//lib/webrick/httprequest.rb#137 + def attributes; end + + # Returns the request body. + # + # source://webrick//lib/webrick/httprequest.rb#255 + def body(&block); end + + # Prepares the HTTPRequest object for use as the + # source for IO.copy_stream + # + # source://webrick//lib/webrick/httprequest.rb#265 + def body_reader; end + + # The content-length header + # + # source://webrick//lib/webrick/httprequest.rb#304 + def content_length; end + + # The content-type header + # + # source://webrick//lib/webrick/httprequest.rb#311 + def content_type; end + + # Generate HTTP/1.1 100 continue response if the client expects it, + # otherwise does nothing. + # + # source://webrick//lib/webrick/httprequest.rb#245 + def continue; end + + # The parsed request cookies + # + # source://webrick//lib/webrick/httprequest.rb#95 + def cookies; end + + # Iterates over the request headers + # + # source://webrick//lib/webrick/httprequest.rb#328 + def each; end + + # Consumes any remaining body and updates keep-alive status + # + # source://webrick//lib/webrick/httprequest.rb#390 + def fixup; end + + # The parsed header of the request + # + # source://webrick//lib/webrick/httprequest.rb#90 + def header; end + + # The host this request is for + # + # source://webrick//lib/webrick/httprequest.rb#340 + def host; end + + # The HTTP version of the request + # + # source://webrick//lib/webrick/httprequest.rb#51 + def http_version; end + + # Is this a keep-alive connection? + # + # source://webrick//lib/webrick/httprequest.rb#142 + def keep_alive; end + + # Should the connection this request was made on be kept alive? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/httprequest.rb#375 + def keep_alive?; end + + # This method provides the metavariables defined by the revision 3 + # of "The WWW Common Gateway Interface Version 1.1" + # To browse the current document of CGI Version 1.1, see below: + # https://www.rfc-editor.org/rfc/rfc3875 + # + # source://webrick//lib/webrick/httprequest.rb#407 + def meta_vars; end + + # Parses a request from +socket+. This is called internally by + # WEBrick::HTTPServer. + # + # source://webrick//lib/webrick/httprequest.rb#193 + def parse(socket = T.unsafe(nil)); end + + # The request path + # + # source://webrick//lib/webrick/httprequest.rb#63 + def path; end + + # The path info (CGI variable) + # + # source://webrick//lib/webrick/httprequest.rb#73 + def path_info; end + + # The path info (CGI variable) + # + # source://webrick//lib/webrick/httprequest.rb#73 + def path_info=(_arg0); end + + # The socket address of the client + # + # source://webrick//lib/webrick/httprequest.rb#132 + def peeraddr; end + + # The port this request is for + # + # source://webrick//lib/webrick/httprequest.rb#347 + def port; end + + # Request query as a Hash + # + # source://webrick//lib/webrick/httprequest.rb#294 + def query; end + + # The query from the URI of the request + # + # source://webrick//lib/webrick/httprequest.rb#78 + def query_string; end + + # The query from the URI of the request + # + # source://webrick//lib/webrick/httprequest.rb#78 + def query_string=(_arg0); end + + # The raw header of the request + # + # source://webrick//lib/webrick/httprequest.rb#85 + def raw_header; end + + # for IO.copy_stream. + # + # source://webrick//lib/webrick/httprequest.rb#278 + def readpartial(size, buf = T.unsafe(nil)); end + + # The client's IP address + # + # source://webrick//lib/webrick/httprequest.rb#361 + def remote_ip; end + + # The complete request line such as: + # + # GET / HTTP/1.1 + # + # source://webrick//lib/webrick/httprequest.rb#36 + def request_line; end + + # The request method, GET, POST, PUT, etc. + # + # source://webrick//lib/webrick/httprequest.rb#41 + def request_method; end + + # The local time this request was received + # + # source://webrick//lib/webrick/httprequest.rb#147 + def request_time; end + + # The parsed URI of the request + # + # source://webrick//lib/webrick/httprequest.rb#58 + def request_uri; end + + # The script name (CGI variable) + # + # source://webrick//lib/webrick/httprequest.rb#68 + def script_name; end + + # The script name (CGI variable) + # + # source://webrick//lib/webrick/httprequest.rb#68 + def script_name=(_arg0); end + + # The server name this request is for + # + # source://webrick//lib/webrick/httprequest.rb#354 + def server_name; end + + # Is this an SSL request? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/httprequest.rb#368 + def ssl?; end + + # source://webrick//lib/webrick/httprequest.rb#379 + def to_s; end + + # The unparsed URI of the request + # + # source://webrick//lib/webrick/httprequest.rb#46 + def unparsed_uri; end + + # The remote user (CGI variable) + # + # source://webrick//lib/webrick/httprequest.rb#122 + def user; end + + # The remote user (CGI variable) + # + # source://webrick//lib/webrick/httprequest.rb#122 + def user=(_arg0); end + + private + + # source://webrick//lib/webrick/httprequest.rb#594 + def _read_data(io, method, *arg); end + + # source://webrick//lib/webrick/httprequest.rb#527 + def parse_host_request_line(host); end + + # source://webrick//lib/webrick/httprequest.rb#614 + def parse_query; end + + # source://webrick//lib/webrick/httprequest.rb#503 + def parse_uri(str, scheme = T.unsafe(nil)); end + + # source://webrick//lib/webrick/httprequest.rb#531 + def read_body(socket, block); end + + # source://webrick//lib/webrick/httprequest.rb#559 + def read_chunk_size(socket); end + + # source://webrick//lib/webrick/httprequest.rb#570 + def read_chunked(socket, block); end + + # source://webrick//lib/webrick/httprequest.rb#610 + def read_data(io, size); end + + # source://webrick//lib/webrick/httprequest.rb#471 + def read_header(socket); end + + # source://webrick//lib/webrick/httprequest.rb#606 + def read_line(io, size = T.unsafe(nil)); end + + # @raise [HTTPStatus::EOFError] + # + # source://webrick//lib/webrick/httprequest.rb#451 + def read_request_line(socket); end + + # It's said that all X-Forwarded-* headers will contain more than one + # (comma-separated) value if the original request already contained one of + # these headers. Since we could use these values as Host header, we choose + # the initial(first) value. (apr_table_mergen() adds new value after the + # existing value with ", " prefix) + # + # source://webrick//lib/webrick/httprequest.rb#642 + def setup_forwarded_info; end +end + +# source://webrick//lib/webrick/httprequest.rb#526 +WEBrick::HTTPRequest::HOST_PATTERN = T.let(T.unsafe(nil), Regexp) + +# same as Mongrel, Thin and Puma +# +# source://webrick//lib/webrick/httprequest.rb#449 +WEBrick::HTTPRequest::MAX_HEADER_LENGTH = T.let(T.unsafe(nil), Integer) + +# An HTTP response. This is filled in by the service or do_* methods of a +# WEBrick HTTP Servlet. +# +# source://webrick//lib/webrick/httpresponse.rb#24 +class WEBrick::HTTPResponse + # Creates a new HTTP response object. WEBrick::Config::HTTP is the + # default configuration. + # + # @return [HTTPResponse] a new instance of HTTPResponse + # + # source://webrick//lib/webrick/httpresponse.rb#117 + def initialize(config); end + + # Retrieves the response header +field+ + # + # source://webrick//lib/webrick/httpresponse.rb#155 + def [](field); end + + # Sets the response header +field+ to +value+ + # + # source://webrick//lib/webrick/httpresponse.rb#162 + def []=(field, value); end + + # Body may be: + # * a String; + # * an IO-like object that responds to +#read+ and +#readpartial+; + # * a Proc-like object that responds to +#call+. + # + # In the latter case, either #chunked= should be set to +true+, + # or header['content-length'] explicitly provided. + # Example: + # + # server.mount_proc '/' do |req, res| + # res.chunked = true + # # or + # # res.header['content-length'] = 10 + # res.body = proc { |out| out.write(Time.now.to_s) } + # end + # + # source://webrick//lib/webrick/httpresponse.rb#70 + def body; end + + # Body may be: + # * a String; + # * an IO-like object that responds to +#read+ and +#readpartial+; + # * a Proc-like object that responds to +#call+. + # + # In the latter case, either #chunked= should be set to +true+, + # or header['content-length'] explicitly provided. + # Example: + # + # server.mount_proc '/' do |req, res| + # res.chunked = true + # # or + # # res.header['content-length'] = 10 + # res.body = proc { |out| out.write(Time.now.to_s) } + # end + # + # source://webrick//lib/webrick/httpresponse.rb#70 + def body=(_arg0); end + + # Enables chunked transfer encoding. + # + # source://webrick//lib/webrick/httpresponse.rb#214 + def chunked=(val); end + + # Will this response body be returned using chunked transfer-encoding? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/httpresponse.rb#207 + def chunked?; end + + # Configuration for this response + # + # source://webrick//lib/webrick/httpresponse.rb#101 + def config; end + + # The content-length header + # + # source://webrick//lib/webrick/httpresponse.rb#170 + def content_length; end + + # Sets the content-length header to +len+ + # + # source://webrick//lib/webrick/httpresponse.rb#179 + def content_length=(len); end + + # The content-type header + # + # source://webrick//lib/webrick/httpresponse.rb#186 + def content_type; end + + # Sets the content-type header to +type+ + # + # source://webrick//lib/webrick/httpresponse.rb#193 + def content_type=(type); end + + # Response cookies + # + # source://webrick//lib/webrick/httpresponse.rb#46 + def cookies; end + + # Iterates over each header in the response + # + # source://webrick//lib/webrick/httpresponse.rb#200 + def each; end + + # Filename of the static file in this response. Only used by the + # FileHandler servlet. + # + # source://webrick//lib/webrick/httpresponse.rb#91 + def filename; end + + # Filename of the static file in this response. Only used by the + # FileHandler servlet. + # + # source://webrick//lib/webrick/httpresponse.rb#91 + def filename=(_arg0); end + + # Response header + # + # source://webrick//lib/webrick/httpresponse.rb#41 + def header; end + + # HTTP Response version + # + # source://webrick//lib/webrick/httpresponse.rb#31 + def http_version; end + + # Is this a keep-alive response? + # + # source://webrick//lib/webrick/httpresponse.rb#96 + def keep_alive; end + + # Is this a keep-alive response? + # + # source://webrick//lib/webrick/httpresponse.rb#96 + def keep_alive=(_arg0); end + + # Will this response's connection be kept alive? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/httpresponse.rb#221 + def keep_alive?; end + + # source://webrick//lib/webrick/httpresponse.rb#325 + def make_body_tempfile; end + + # Response reason phrase ("OK") + # + # source://webrick//lib/webrick/httpresponse.rb#51 + def reason_phrase; end + + # Response reason phrase ("OK") + # + # source://webrick//lib/webrick/httpresponse.rb#51 + def reason_phrase=(_arg0); end + + # source://webrick//lib/webrick/httpresponse.rb#343 + def remove_body_tempfile; end + + # Request HTTP version for this response + # + # source://webrick//lib/webrick/httpresponse.rb#85 + def request_http_version; end + + # Request HTTP version for this response + # + # source://webrick//lib/webrick/httpresponse.rb#85 + def request_http_version=(_arg0); end + + # Request method for this response + # + # source://webrick//lib/webrick/httpresponse.rb#75 + def request_method; end + + # Request method for this response + # + # source://webrick//lib/webrick/httpresponse.rb#75 + def request_method=(_arg0); end + + # Request URI for this response + # + # source://webrick//lib/webrick/httpresponse.rb#80 + def request_uri; end + + # Request URI for this response + # + # source://webrick//lib/webrick/httpresponse.rb#80 + def request_uri=(_arg0); end + + # Sends the body on +socket+ + # + # source://webrick//lib/webrick/httpresponse.rb#378 + def send_body(socket); end + + # Sends the headers on +socket+ + # + # source://webrick//lib/webrick/httpresponse.rb#355 + def send_header(socket); end + + # Sends the response on +socket+ + # + # source://webrick//lib/webrick/httpresponse.rb#238 + def send_response(socket); end + + # Bytes sent in this response + # + # source://webrick//lib/webrick/httpresponse.rb#106 + def sent_size; end + + # Creates an error page for exception +ex+ with an optional +backtrace+ + # + # source://webrick//lib/webrick/httpresponse.rb#405 + def set_error(ex, backtrace = T.unsafe(nil)); end + + # Redirects to +url+ with a WEBrick::HTTPStatus::Redirect +status+. + # + # Example: + # + # res.set_redirect WEBrick::HTTPStatus::TemporaryRedirect + # + # source://webrick//lib/webrick/httpresponse.rb#395 + def set_redirect(status, url); end + + # Sets up the headers for sending + # + # source://webrick//lib/webrick/httpresponse.rb#255 + def setup_header; end + + # Response status code (200) + # + # source://webrick//lib/webrick/httpresponse.rb#36 + def status; end + + # Sets the response's status to the +status+ code + # + # source://webrick//lib/webrick/httpresponse.rb#147 + def status=(status); end + + # The response's HTTP status line + # + # source://webrick//lib/webrick/httpresponse.rb#140 + def status_line; end + + # Set the response body proc as an streaming/upgrade response. + # + # source://webrick//lib/webrick/httpresponse.rb#111 + def upgrade; end + + # Sets the response to be a streaming/upgrade response. + # This will disable keep-alive and chunked transfer encoding. + # + # source://webrick//lib/webrick/httpresponse.rb#229 + def upgrade!(protocol); end + + # Set the response body proc as an streaming/upgrade response. + # + # source://webrick//lib/webrick/httpresponse.rb#111 + def upgrade=(_arg0); end + + private + + # preserved for compatibility with some 3rd-party handlers + # + # source://webrick//lib/webrick/httpresponse.rb#581 + def _write_data(socket, data); end + + # source://webrick//lib/webrick/httpresponse.rb#432 + def check_header(header_value); end + + # :stopdoc: + # + # source://webrick//lib/webrick/httpresponse.rb#443 + def error_body(backtrace, ex, host, port); end + + # source://webrick//lib/webrick/httpresponse.rb#473 + def send_body_io(socket); end + + # source://webrick//lib/webrick/httpresponse.rb#535 + def send_body_proc(socket); end + + # source://webrick//lib/webrick/httpresponse.rb#513 + def send_body_string(socket); end +end + +# source://webrick//lib/webrick/httpresponse.rb#555 +class WEBrick::HTTPResponse::ChunkedWrapper + # @return [ChunkedWrapper] a new instance of ChunkedWrapper + # + # source://webrick//lib/webrick/httpresponse.rb#556 + def initialize(socket, resp); end + + # source://webrick//lib/webrick/httpresponse.rb#574 + def <<(*buf); end + + # source://webrick//lib/webrick/httpresponse.rb#561 + def write(buf); end +end + +# An HTTP Server +# +# source://webrick//lib/webrick/httpserver.rb#27 +class WEBrick::HTTPServer < ::WEBrick::GenericServer + # Creates a new HTTP server according to +config+ + # + # An HTTP server uses the following attributes: + # + # :AccessLog:: An array of access logs. See WEBrick::AccessLog + # :BindAddress:: Local address for the server to bind to + # :DocumentRoot:: Root path to serve files from + # :DocumentRootOptions:: Options for the default HTTPServlet::FileHandler + # :HTTPVersion:: The HTTP version of this server + # :Port:: Port to listen on + # :RequestCallback:: Called with a request and response before each + # request is serviced. + # :RequestTimeout:: Maximum time to wait between requests + # :ServerAlias:: Array of alternate names for this server for virtual + # hosting + # :ServerName:: Name for this server for virtual hosting + # + # @return [HTTPServer] a new instance of HTTPServer + # + # source://webrick//lib/webrick/httpserver.rb#46 + def initialize(config = T.unsafe(nil), default = T.unsafe(nil)); end + + # Logs +req+ and +res+ in the access logs. +config+ is used for the + # server name. + # + # source://webrick//lib/webrick/httpserver.rb#220 + def access_log(config, req, res); end + + # Creates the HTTPRequest used when handling the HTTP + # request. Can be overridden by subclasses. + # + # source://webrick//lib/webrick/httpserver.rb#230 + def create_request(with_webrick_config); end + + # Creates the HTTPResponse used when handling the HTTP + # request. Can be overridden by subclasses. + # + # source://webrick//lib/webrick/httpserver.rb#237 + def create_response(with_webrick_config); end + + # The default OPTIONS request handler says GET, HEAD, POST and OPTIONS + # requests are allowed. + # + # source://webrick//lib/webrick/httpserver.rb#147 + def do_OPTIONS(req, res); end + + # Finds the appropriate virtual host to handle +req+ + # + # source://webrick//lib/webrick/httpserver.rb#207 + def lookup_server(req); end + + # Mounts +servlet+ on +dir+ passing +options+ to the servlet at creation + # time + # + # source://webrick//lib/webrick/httpserver.rb#155 + def mount(dir, servlet, *options); end + + # Mounts +proc+ or +block+ on +dir+ and calls it with a + # WEBrick::HTTPRequest and WEBrick::HTTPResponse + # + # @raise [HTTPServerError] + # + # source://webrick//lib/webrick/httpserver.rb#164 + def mount_proc(dir, proc = T.unsafe(nil), &block); end + + # Processes requests on +sock+ + # + # source://webrick//lib/webrick/httpserver.rb#69 + def run(sock); end + + # Finds a servlet for +path+ + # + # source://webrick//lib/webrick/httpserver.rb#182 + def search_servlet(path); end + + # Services +req+ and fills in +res+ + # + # @raise [HTTPStatus::NotFound] + # + # source://webrick//lib/webrick/httpserver.rb#125 + def service(req, res); end + + # Unmounts +dir+ + # + # source://webrick//lib/webrick/httpserver.rb#173 + def umount(dir); end + + # Unmounts +dir+ + # + # source://webrick//lib/webrick/httpserver.rb#173 + def unmount(dir); end + + # Adds +server+ as a virtual host. + # + # source://webrick//lib/webrick/httpserver.rb#193 + def virtual_host(server); end +end + +# Mount table for the path a servlet is mounted on in the directory space +# of the server. Users of WEBrick can only access this indirectly via +# WEBrick::HTTPServer#mount, WEBrick::HTTPServer#unmount and +# WEBrick::HTTPServer#search_servlet +# +# source://webrick//lib/webrick/httpserver.rb#247 +class WEBrick::HTTPServer::MountTable + # @return [MountTable] a new instance of MountTable + # + # source://webrick//lib/webrick/httpserver.rb#248 + def initialize; end + + # source://webrick//lib/webrick/httpserver.rb#253 + def [](dir); end + + # source://webrick//lib/webrick/httpserver.rb#258 + def []=(dir, val); end + + # source://webrick//lib/webrick/httpserver.rb#265 + def delete(dir); end + + # source://webrick//lib/webrick/httpserver.rb#272 + def scan(path); end + + private + + # source://webrick//lib/webrick/httpserver.rb#279 + def compile; end + + # source://webrick//lib/webrick/httpserver.rb#287 + def normalize(dir); end +end + +# AbstractServlet allows HTTP server modules to be reused across multiple +# servers and allows encapsulation of functionality. +# +# By default a servlet will respond to GET, HEAD (through an alias to GET) +# and OPTIONS requests. +# +# By default a new servlet is initialized for every request. A servlet +# instance can be reused by overriding ::get_instance in the +# AbstractServlet subclass. +# +# == A Simple Servlet +# +# class Simple < WEBrick::HTTPServlet::AbstractServlet +# def do_GET request, response +# status, content_type, body = do_stuff_with request +# +# response.status = status +# response['Content-Type'] = content_type +# response.body = body +# end +# +# def do_stuff_with request +# return 200, 'text/plain', 'you got a page' +# end +# end +# +# This servlet can be mounted on a server at a given path: +# +# server.mount '/simple', Simple +# +# == Servlet Configuration +# +# Servlets can be configured via initialize. The first argument is the +# HTTP server the servlet is being initialized for. +# +# class Configurable < Simple +# def initialize server, color, size +# super server +# @color = color +# @size = size +# end +# +# def do_stuff_with request +# content = "

Hello, World!" +# +# return 200, "text/html", content +# end +# end +# +# This servlet must be provided two arguments at mount time: +# +# server.mount '/configurable', Configurable, 'red', '2em' +# +# source://webrick//lib/webrick/httpservlet/abstract.rb#76 +class WEBrick::HTTPServlet::AbstractServlet + # Initializes a new servlet for +server+ using +options+ which are + # stored as-is in +@options+. +@logger+ is also provided. + # + # @return [AbstractServlet] a new instance of AbstractServlet + # + # source://webrick//lib/webrick/httpservlet/abstract.rb#91 + def initialize(server, *options); end + + # Raises a NotFound exception + # + # @raise [HTTPStatus::NotFound] + # + # source://webrick//lib/webrick/httpservlet/abstract.rb#115 + def do_GET(req, res); end + + # Dispatches to do_GET + # + # source://webrick//lib/webrick/httpservlet/abstract.rb#122 + def do_HEAD(req, res); end + + # Returns the allowed HTTP request methods + # + # source://webrick//lib/webrick/httpservlet/abstract.rb#129 + def do_OPTIONS(req, res); end + + # Dispatches to a +do_+ method based on +req+ if such a method is + # available. (+do_GET+ for a GET request). Raises a MethodNotAllowed + # exception if the method is not implemented. + # + # source://webrick//lib/webrick/httpservlet/abstract.rb#102 + def service(req, res); end + + private + + # Redirects to a path ending in / + # + # source://webrick//lib/webrick/httpservlet/abstract.rb#140 + def redirect_to_directory_uri(req, res); end + + class << self + # Factory for servlet instances that will handle a request from +server+ + # using +options+ from the mount point. By default a new servlet + # instance is created for every call. + # + # source://webrick//lib/webrick/httpservlet/abstract.rb#83 + def get_instance(server, *options); end + end +end + +# Servlet for handling CGI scripts +# +# Example: +# +# server.mount('/cgi/my_script', WEBrick::HTTPServlet::CGIHandler, +# '/path/to/my_script') +# +# source://webrick//lib/webrick/httpservlet/cgihandler.rb#28 +class WEBrick::HTTPServlet::CGIHandler < ::WEBrick::HTTPServlet::AbstractServlet + # Creates a new CGI script servlet for the script at +name+ + # + # @return [CGIHandler] a new instance of CGIHandler + # + # source://webrick//lib/webrick/httpservlet/cgihandler.rb#36 + def initialize(server, name); end + + # :stopdoc: + # + # @raise [HTTPStatus::InternalServerError] + # + # source://webrick//lib/webrick/httpservlet/cgihandler.rb#50 + def do_GET(req, res); end + + # :stopdoc: + # + # @raise [HTTPStatus::InternalServerError] + # + # source://webrick//lib/webrick/httpservlet/cgihandler.rb#50 + def do_POST(req, res); end +end + +# source://webrick//lib/webrick/httpservlet/cgihandler.rb#31 +WEBrick::HTTPServlet::CGIHandler::CGIRunnerArray = T.let(T.unsafe(nil), Array) + +# Servlet for serving a single file. You probably want to use the +# FileHandler servlet instead as it handles directories and fancy indexes. +# +# Example: +# +# server.mount('/my_page.txt', WEBrick::HTTPServlet::DefaultFileHandler, +# '/path/to/my_page.txt') +# +# This servlet handles If-Modified-Since and Range requests. +# +# source://webrick//lib/webrick/httpservlet/filehandler.rb#32 +class WEBrick::HTTPServlet::DefaultFileHandler < ::WEBrick::HTTPServlet::AbstractServlet + # Creates a DefaultFileHandler instance for the file at +local_path+. + # + # @return [DefaultFileHandler] a new instance of DefaultFileHandler + # + # source://webrick//lib/webrick/httpservlet/filehandler.rb#37 + def initialize(server, local_path); end + + # :stopdoc: + # + # source://webrick//lib/webrick/httpservlet/filehandler.rb#44 + def do_GET(req, res); end + + # source://webrick//lib/webrick/httpservlet/filehandler.rb#118 + def make_partial_content(req, res, filename, filesize); end + + # returns a lambda for webrick/httpresponse.rb send_body_proc + # + # source://webrick//lib/webrick/httpservlet/filehandler.rb#90 + def multipart_body(body, parts, boundary, mtype, filesize); end + + # @return [Boolean] + # + # source://webrick//lib/webrick/httpservlet/filehandler.rb#64 + def not_modified?(req, res, mtime, etag); end + + # source://webrick//lib/webrick/httpservlet/filehandler.rb#155 + def prepare_range(range, filesize); end +end + +# ERBHandler evaluates an ERB file and returns the result. This handler +# is automatically used if there are .rhtml files in a directory served by +# the FileHandler. +# +# ERBHandler supports GET and POST methods. +# +# The ERB file is evaluated with the local variables +servlet_request+ and +# +servlet_response+ which are a WEBrick::HTTPRequest and +# WEBrick::HTTPResponse respectively. +# +# Example .rhtml file: +# +# Request to <%= servlet_request.request_uri %> +# +# Query params <%= servlet_request.query.inspect %> +# +# source://webrick//lib/webrick/httpservlet/erbhandler.rb#36 +class WEBrick::HTTPServlet::ERBHandler < ::WEBrick::HTTPServlet::AbstractServlet + # Creates a new ERBHandler on +server+ that will evaluate and serve the + # ERB file +name+ + # + # @return [ERBHandler] a new instance of ERBHandler + # + # source://webrick//lib/webrick/httpservlet/erbhandler.rb#42 + def initialize(server, name); end + + # Handles GET requests + # + # source://webrick//lib/webrick/httpservlet/erbhandler.rb#50 + def do_GET(req, res); end + + # Handles GET requests + # + # Handles POST requests + # + # source://webrick//lib/webrick/httpservlet/erbhandler.rb#50 + def do_POST(req, res); end + + private + + # Evaluates +erb+ providing +servlet_request+ and +servlet_response+ as + # local variables. + # + # source://webrick//lib/webrick/httpservlet/erbhandler.rb#79 + def evaluate(erb, servlet_request, servlet_response); end +end + +# Serves a directory including fancy indexing and a variety of other +# options. +# +# Example: +# +# server.mount('/assets', WEBrick::HTTPServlet::FileHandler, +# '/path/to/assets') +# +# source://webrick//lib/webrick/httpservlet/filehandler.rb#175 +class WEBrick::HTTPServlet::FileHandler < ::WEBrick::HTTPServlet::AbstractServlet + # Creates a FileHandler servlet on +server+ that serves files starting + # at directory +root+ + # + # +options+ may be a Hash containing keys from + # WEBrick::Config::FileHandler or +true+ or +false+. + # + # If +options+ is true or false then +:FancyIndexing+ is enabled or + # disabled respectively. + # + # @return [FileHandler] a new instance of FileHandler + # + # source://webrick//lib/webrick/httpservlet/filehandler.rb#203 + def initialize(server, root, options = T.unsafe(nil), default = T.unsafe(nil)); end + + # source://webrick//lib/webrick/httpservlet/filehandler.rb#245 + def do_GET(req, res); end + + # source://webrick//lib/webrick/httpservlet/filehandler.rb#257 + def do_OPTIONS(req, res); end + + # source://webrick//lib/webrick/httpservlet/filehandler.rb#251 + def do_POST(req, res); end + + # source://webrick//lib/webrick/httpservlet/filehandler.rb#224 + def service(req, res); end + + # :stopdoc: + # + # source://webrick//lib/webrick/httpservlet/filehandler.rb#215 + def set_filesystem_encoding(str); end + + private + + # source://webrick//lib/webrick/httpservlet/filehandler.rb#416 + def call_callback(callback_name, req, res); end + + # source://webrick//lib/webrick/httpservlet/filehandler.rb#369 + def check_filename(req, res, name); end + + # @raise [HTTPStatus::NotFound] + # + # source://webrick//lib/webrick/httpservlet/filehandler.rb#309 + def exec_handler(req, res); end + + # source://webrick//lib/webrick/httpservlet/filehandler.rb#322 + def get_handler(req, res); end + + # @return [Boolean] + # + # source://webrick//lib/webrick/httpservlet/filehandler.rb#428 + def nondisclosure_name?(name); end + + # source://webrick//lib/webrick/httpservlet/filehandler.rb#286 + def prevent_directory_traversal(req, res); end + + # source://webrick//lib/webrick/httpservlet/filehandler.rb#394 + def search_file(req, res, basename); end + + # source://webrick//lib/webrick/httpservlet/filehandler.rb#385 + def search_index_file(req, res); end + + # source://webrick//lib/webrick/httpservlet/filehandler.rb#437 + def set_dir_list(req, res); end + + # source://webrick//lib/webrick/httpservlet/filehandler.rb#335 + def set_filename(req, res); end + + # source://webrick//lib/webrick/httpservlet/filehandler.rb#376 + def shift_path_info(req, res, path_info, base = T.unsafe(nil)); end + + # @return [Boolean] + # + # source://webrick//lib/webrick/httpservlet/filehandler.rb#277 + def trailing_pathsep?(path); end + + # @return [Boolean] + # + # source://webrick//lib/webrick/httpservlet/filehandler.rb#422 + def windows_ambiguous_name?(name); end + + class << self + # Allow custom handling of requests for files with +suffix+ by class + # +handler+ + # + # source://webrick//lib/webrick/httpservlet/filehandler.rb#182 + def add_handler(suffix, handler); end + + # Remove custom handling of requests for files with +suffix+ + # + # source://webrick//lib/webrick/httpservlet/filehandler.rb#189 + def remove_handler(suffix); end + end +end + +# Mounts a proc at a path that accepts a request and response. +# +# Instead of mounting this servlet with WEBrick::HTTPServer#mount use +# WEBrick::HTTPServer#mount_proc: +# +# server.mount_proc '/' do |req, res| +# res.body = 'it worked!' +# res.status = 200 +# end +# +# source://webrick//lib/webrick/httpservlet/prochandler.rb#28 +class WEBrick::HTTPServlet::ProcHandler < ::WEBrick::HTTPServlet::AbstractServlet + # @return [ProcHandler] a new instance of ProcHandler + # + # source://webrick//lib/webrick/httpservlet/prochandler.rb#34 + def initialize(proc); end + + # source://webrick//lib/webrick/httpservlet/prochandler.rb#38 + def do_GET(request, response); end + + # source://webrick//lib/webrick/httpservlet/prochandler.rb#38 + def do_POST(request, response); end + + # source://webrick//lib/webrick/httpservlet/prochandler.rb#38 + def do_PUT(request, response); end + + # :stopdoc: + # + # source://webrick//lib/webrick/httpservlet/prochandler.rb#30 + def get_instance(server, *options); end +end + +# This module is used to manager HTTP status codes. +# +# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for more +# information. +# +# source://webrick//lib/webrick/httpstatus.rb#21 +module WEBrick::HTTPStatus + private + + # Is +code+ a client error status? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/httpstatus.rb#170 + def client_error?(code); end + + # Is +code+ an error status? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/httpstatus.rb#164 + def error?(code); end + + # Is +code+ an informational status? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/httpstatus.rb#146 + def info?(code); end + + # Returns the description corresponding to the HTTP status +code+ + # + # WEBrick::HTTPStatus.reason_phrase 404 + # => "Not Found" + # + # source://webrick//lib/webrick/httpstatus.rb#140 + def reason_phrase(code); end + + # Is +code+ a redirection status? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/httpstatus.rb#158 + def redirect?(code); end + + # Is +code+ a server error status? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/httpstatus.rb#176 + def server_error?(code); end + + # Is +code+ a successful status? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/httpstatus.rb#152 + def success?(code); end + + class << self + # Returns the status class corresponding to +code+ + # + # WEBrick::HTTPStatus[302] + # => WEBrick::HTTPStatus::NotFound + # + # source://webrick//lib/webrick/httpstatus.rb#186 + def [](code); end + + # Is +code+ a client error status? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/httpstatus.rb#170 + def client_error?(code); end + + # Is +code+ an error status? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/httpstatus.rb#164 + def error?(code); end + + # Is +code+ an informational status? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/httpstatus.rb#146 + def info?(code); end + + # Returns the description corresponding to the HTTP status +code+ + # + # WEBrick::HTTPStatus.reason_phrase 404 + # => "Not Found" + # + # source://webrick//lib/webrick/httpstatus.rb#140 + def reason_phrase(code); end + + # Is +code+ a redirection status? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/httpstatus.rb#158 + def redirect?(code); end + + # Is +code+ a server error status? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/httpstatus.rb#176 + def server_error?(code); end + + # Is +code+ a successful status? + # + # @return [Boolean] + # + # source://webrick//lib/webrick/httpstatus.rb#152 + def success?(code); end + end +end + +# Root of the HTTP status class hierarchy +# +# source://webrick//lib/webrick/httpstatus.rb#25 +class WEBrick::HTTPStatus::Status < ::StandardError + # Returns the HTTP status code + # + # source://webrick//lib/webrick/httpstatus.rb#31 + def code; end + + # Returns the HTTP status description + # + # source://webrick//lib/webrick/httpstatus.rb#34 + def reason_phrase; end + + # Returns the HTTP status code + # + # source://webrick//lib/webrick/httpstatus.rb#31 + def to_i; end + + class << self + # source://webrick//lib/webrick/httpstatus.rb#27 + def code; end + + # source://webrick//lib/webrick/httpstatus.rb#27 + def reason_phrase; end + end +end + +# HTTPUtils provides utility methods for working with the HTTP protocol. +# +# This module is generally used internally by WEBrick +# +# source://webrick//lib/webrick/httputils.rb#25 +module WEBrick::HTTPUtils + private + + # source://webrick//lib/webrick/httputils.rb#474 + def _escape(str, regex); end + + # :stopdoc: + # + # source://webrick//lib/webrick/httputils.rb#472 + def _make_regex(str); end + + # source://webrick//lib/webrick/httputils.rb#473 + def _make_regex!(str); end + + # source://webrick//lib/webrick/httputils.rb#480 + def _unescape(str, regex); end + + # Removes quotes and escapes from +str+ + # + # source://webrick//lib/webrick/httputils.rb#254 + def dequote(str); end + + # Escapes HTTP reserved and unwise characters in +str+ + # + # source://webrick//lib/webrick/httputils.rb#498 + def escape(str); end + + # Escapes 8 bit characters in +str+ + # + # source://webrick//lib/webrick/httputils.rb#539 + def escape8bit(str); end + + # Escapes form reserved characters in +str+ + # + # source://webrick//lib/webrick/httputils.rb#512 + def escape_form(str); end + + # Escapes path +str+ + # + # source://webrick//lib/webrick/httputils.rb#528 + def escape_path(str); end + + # Loads Apache-compatible mime.types in +file+. + # + # source://webrick//lib/webrick/httputils.rb#122 + def load_mime_types(file); end + + # Returns the mime type of +filename+ from the list in +mime_tab+. If no + # mime type was found application/octet-stream is returned. + # + # source://webrick//lib/webrick/httputils.rb#144 + def mime_type(filename, mime_tab); end + + # Normalizes a request path. Raises an exception if the path cannot be + # normalized. + # + # source://webrick//lib/webrick/httputils.rb#31 + def normalize_path(path); end + + # Parses form data in +io+ with the given +boundary+ + # + # source://webrick//lib/webrick/httputils.rb#426 + def parse_form_data(io, boundary); end + + # source://webrick//lib/webrick/httputils.rb#171 + def parse_header(raw); end + + # Parses the query component of a URI in +str+ + # + # source://webrick//lib/webrick/httputils.rb#402 + def parse_query(str); end + + # Parses q values in +value+ as used in Accept headers. + # + # source://webrick//lib/webrick/httputils.rb#233 + def parse_qvalues(value); end + + # Parses a Range header value +ranges_specifier+ + # + # source://webrick//lib/webrick/httputils.rb#215 + def parse_range_header(ranges_specifier); end + + # Quotes and escapes quotes in +str+ + # + # source://webrick//lib/webrick/httputils.rb#264 + def quote(str); end + + # Splits a header value +str+ according to HTTP specification. + # + # source://webrick//lib/webrick/httputils.rb#206 + def split_header_value(str); end + + # Unescapes HTTP reserved and unwise characters in +str+ + # + # source://webrick//lib/webrick/httputils.rb#505 + def unescape(str); end + + # Unescapes form reserved characters in +str+ + # + # source://webrick//lib/webrick/httputils.rb#521 + def unescape_form(str); end + + class << self + # source://webrick//lib/webrick/httputils.rb#474 + def _escape(str, regex); end + + # :stopdoc: + # + # source://webrick//lib/webrick/httputils.rb#472 + def _make_regex(str); end + + # source://webrick//lib/webrick/httputils.rb#473 + def _make_regex!(str); end + + # source://webrick//lib/webrick/httputils.rb#480 + def _unescape(str, regex); end + + # Removes quotes and escapes from +str+ + # + # source://webrick//lib/webrick/httputils.rb#254 + def dequote(str); end + + # Escapes HTTP reserved and unwise characters in +str+ + # + # source://webrick//lib/webrick/httputils.rb#498 + def escape(str); end + + # Escapes 8 bit characters in +str+ + # + # source://webrick//lib/webrick/httputils.rb#539 + def escape8bit(str); end + + # Escapes form reserved characters in +str+ + # + # source://webrick//lib/webrick/httputils.rb#512 + def escape_form(str); end + + # Escapes path +str+ + # + # source://webrick//lib/webrick/httputils.rb#528 + def escape_path(str); end + + # Loads Apache-compatible mime.types in +file+. + # + # source://webrick//lib/webrick/httputils.rb#122 + def load_mime_types(file); end + + # Returns the mime type of +filename+ from the list in +mime_tab+. If no + # mime type was found application/octet-stream is returned. + # + # source://webrick//lib/webrick/httputils.rb#144 + def mime_type(filename, mime_tab); end + + # Normalizes a request path. Raises an exception if the path cannot be + # normalized. + # + # source://webrick//lib/webrick/httputils.rb#31 + def normalize_path(path); end + + # Parses form data in +io+ with the given +boundary+ + # + # source://webrick//lib/webrick/httputils.rb#426 + def parse_form_data(io, boundary); end + + # source://webrick//lib/webrick/httputils.rb#171 + def parse_header(raw); end + + # Parses the query component of a URI in +str+ + # + # source://webrick//lib/webrick/httputils.rb#402 + def parse_query(str); end + + # Parses q values in +value+ as used in Accept headers. + # + # source://webrick//lib/webrick/httputils.rb#233 + def parse_qvalues(value); end + + # Parses a Range header value +ranges_specifier+ + # + # source://webrick//lib/webrick/httputils.rb#215 + def parse_range_header(ranges_specifier); end + + # Quotes and escapes quotes in +str+ + # + # source://webrick//lib/webrick/httputils.rb#264 + def quote(str); end + + # Splits a header value +str+ according to HTTP specification. + # + # source://webrick//lib/webrick/httputils.rb#206 + def split_header_value(str); end + + # Unescapes HTTP reserved and unwise characters in +str+ + # + # source://webrick//lib/webrick/httputils.rb#505 + def unescape(str); end + + # Unescapes form reserved characters in +str+ + # + # source://webrick//lib/webrick/httputils.rb#521 + def unescape_form(str); end + end +end + +# source://webrick//lib/webrick/httputils.rb#161 +class WEBrick::HTTPUtils::CookieHeader < ::Array + # source://webrick//lib/webrick/httputils.rb#162 + def join(separator = T.unsafe(nil)); end +end + +# Stores multipart form data. FormData objects are created when +# WEBrick::HTTPUtils.parse_form_data is called. +# +# source://webrick//lib/webrick/httputils.rb#273 +class WEBrick::HTTPUtils::FormData < ::String + # Creates a new FormData object. + # + # +args+ is an Array of form data entries. One FormData will be created + # for each entry. + # + # This is called by WEBrick::HTTPUtils.parse_form_data for you + # + # @return [FormData] a new instance of FormData + # + # source://webrick//lib/webrick/httputils.rb#298 + def initialize(*args); end + + # Adds +str+ to this FormData which may be the body, a header or a + # header entry. + # + # This is called by WEBrick::HTTPUtils.parse_form_data for you + # + # source://webrick//lib/webrick/httputils.rb#331 + def <<(str); end + + # Retrieves the header at the first entry in +key+ + # + # source://webrick//lib/webrick/httputils.rb#317 + def [](*key); end + + # Adds +data+ at the end of the chain of entries + # + # This is called by WEBrick::HTTPUtils.parse_form_data for you. + # + # source://webrick//lib/webrick/httputils.rb#351 + def append_data(data); end + + # Yields each entry in this FormData + # + # source://webrick//lib/webrick/httputils.rb#366 + def each_data; end + + # The filename of the form data part + # + # source://webrick//lib/webrick/httputils.rb#285 + def filename; end + + # The filename of the form data part + # + # source://webrick//lib/webrick/httputils.rb#285 + def filename=(_arg0); end + + # Returns all the FormData as an Array + # + # source://webrick//lib/webrick/httputils.rb#378 + def list; end + + # The name of the form data part + # + # source://webrick//lib/webrick/httputils.rb#280 + def name; end + + # The name of the form data part + # + # source://webrick//lib/webrick/httputils.rb#280 + def name=(_arg0); end + + # source://webrick//lib/webrick/httputils.rb#287 + def next_data=(_arg0); end + + # Returns all the FormData as an Array + # + # A FormData will behave like an Array + # + # source://webrick//lib/webrick/httputils.rb#378 + def to_ary; end + + # This FormData's body + # + # source://webrick//lib/webrick/httputils.rb#394 + def to_s; end + + protected + + # source://webrick//lib/webrick/httputils.rb#287 + def next_data; end +end + +# source://webrick//lib/webrick/httputils.rb#167 +WEBrick::HTTPUtils::HEADER_CLASSES = T.let(T.unsafe(nil), Hash) + +# Parses an HTTP header +raw+ into a hash of header fields with an Array +# of values. +# +# source://webrick//lib/webrick/httputils.rb#155 +class WEBrick::HTTPUtils::SplitHeader < ::Array + # source://webrick//lib/webrick/httputils.rb#156 + def join(separator = T.unsafe(nil)); end +end + +# Represents an HTTP protocol version +# +# source://webrick//lib/webrick/httpversion.rb#16 +class WEBrick::HTTPVersion + include ::Comparable + + # Creates a new HTTPVersion from +version+. + # + # @return [HTTPVersion] a new instance of HTTPVersion + # + # source://webrick//lib/webrick/httpversion.rb#39 + def initialize(version); end + + # Compares this version with +other+ according to the HTTP specification + # rules. + # + # source://webrick//lib/webrick/httpversion.rb#58 + def <=>(other); end + + # The major protocol version number + # + # source://webrick//lib/webrick/httpversion.rb#22 + def major; end + + # The major protocol version number + # + # source://webrick//lib/webrick/httpversion.rb#22 + def major=(_arg0); end + + # The minor protocol version number + # + # source://webrick//lib/webrick/httpversion.rb#27 + def minor; end + + # The minor protocol version number + # + # source://webrick//lib/webrick/httpversion.rb#27 + def minor=(_arg0); end + + # The HTTP version as show in the HTTP request and response. For example, + # "1.1" + # + # source://webrick//lib/webrick/httpversion.rb#72 + def to_s; end + + class << self + # Converts +version+ into an HTTPVersion + # + # source://webrick//lib/webrick/httpversion.rb#32 + def convert(version); end + end +end + +# A logging class that prepends a timestamp to each message. +# +# source://webrick//lib/webrick/log.rb#134 +class WEBrick::Log < ::WEBrick::BasicLog + # Same as BasicLog#initialize + # + # You can set the timestamp format through #time_format + # + # @return [Log] a new instance of Log + # + # source://webrick//lib/webrick/log.rb#143 + def initialize(log_file = T.unsafe(nil), level = T.unsafe(nil)); end + + # Same as BasicLog#log + # + # source://webrick//lib/webrick/log.rb#150 + def log(level, data); end + + # Format of the timestamp which is applied to each logged line. The + # default is "[%Y-%m-%d %H:%M:%S]" + # + # source://webrick//lib/webrick/log.rb#137 + def time_format; end + + # Format of the timestamp which is applied to each logged line. The + # default is "[%Y-%m-%d %H:%M:%S]" + # + # source://webrick//lib/webrick/log.rb#137 + def time_format=(_arg0); end +end + +# Base server class +# +# source://webrick//lib/webrick/server.rb#26 +class WEBrick::SimpleServer + class << self + # A SimpleServer only yields when you start it + # + # source://webrick//lib/webrick/server.rb#31 + def start; end + end +end + +# source://webrick//lib/webrick/utils.rb#17 +module WEBrick::Utils + private + + # Creates TCP server sockets bound to +address+:+port+ and returns them. + # + # It will create IPV4 and IPV6 sockets on all interfaces. + # + # source://webrick//lib/webrick/utils.rb#56 + def create_listeners(address, port); end + + # The server hostname + # + # source://webrick//lib/webrick/utils.rb#47 + def getservername; end + + # Generates a random string of length +len+ + # + # source://webrick//lib/webrick/utils.rb#79 + def random_string(len); end + + # Sets the close on exec flag for +io+ + # + # source://webrick//lib/webrick/utils.rb#27 + def set_close_on_exec(io); end + + # Sets IO operations on +io+ to be non-blocking + # + # source://webrick//lib/webrick/utils.rb#20 + def set_non_blocking(io); end + + # Changes the process's uid and gid to the ones of +user+ + # + # source://webrick//lib/webrick/utils.rb#34 + def su(user); end + + # Executes the passed block and raises +exception+ if execution takes more + # than +seconds+. + # + # If +seconds+ is zero or nil, simply executes the block + # + # source://webrick//lib/webrick/utils.rb#253 + def timeout(seconds, exception = T.unsafe(nil)); end + + class << self + # Creates TCP server sockets bound to +address+:+port+ and returns them. + # + # It will create IPV4 and IPV6 sockets on all interfaces. + # + # source://webrick//lib/webrick/utils.rb#56 + def create_listeners(address, port); end + + # The server hostname + # + # source://webrick//lib/webrick/utils.rb#47 + def getservername; end + + # Generates a random string of length +len+ + # + # source://webrick//lib/webrick/utils.rb#79 + def random_string(len); end + + # Sets the close on exec flag for +io+ + # + # source://webrick//lib/webrick/utils.rb#27 + def set_close_on_exec(io); end + + # Sets IO operations on +io+ to be non-blocking + # + # source://webrick//lib/webrick/utils.rb#20 + def set_non_blocking(io); end + + # Changes the process's uid and gid to the ones of +user+ + # + # source://webrick//lib/webrick/utils.rb#34 + def su(user); end + + # Executes the passed block and raises +exception+ if execution takes more + # than +seconds+. + # + # If +seconds+ is zero or nil, simply executes the block + # + # source://webrick//lib/webrick/utils.rb#253 + def timeout(seconds, exception = T.unsafe(nil)); end + end +end + +# Class used to manage timeout handlers across multiple threads. +# +# Timeout handlers should be managed by using the class methods which are +# synchronized. +# +# id = TimeoutHandler.register(10, Timeout::Error) +# begin +# sleep 20 +# puts 'foo' +# ensure +# TimeoutHandler.cancel(id) +# end +# +# will raise Timeout::Error +# +# id = TimeoutHandler.register(10, Timeout::Error) +# begin +# sleep 5 +# puts 'foo' +# ensure +# TimeoutHandler.cancel(id) +# end +# +# will print 'foo' +# +# source://webrick//lib/webrick/utils.rb#118 +class WEBrick::Utils::TimeoutHandler + include ::Singleton::SingletonInstanceMethods + include ::Singleton + extend ::Singleton::SingletonClassMethods + + # Creates a new TimeoutHandler. You should use ::register and ::cancel + # instead of creating the timeout handler directly. + # + # @return [TimeoutHandler] a new instance of TimeoutHandler + # + # source://webrick//lib/webrick/utils.rb#148 + def initialize; end + + # Cancels the timeout handler +id+ + # + # source://webrick//lib/webrick/utils.rb#226 + def cancel(thread, id); end + + # Interrupts the timeout handler +id+ and raises +exception+ + # + # source://webrick//lib/webrick/utils.rb#203 + def interrupt(thread, id, exception); end + + # Registers a new timeout handler + # + # +time+:: Timeout in seconds + # +exception+:: Exception to raise when timeout elapsed + # + # source://webrick//lib/webrick/utils.rb#214 + def register(thread, time, exception); end + + # source://webrick//lib/webrick/utils.rb#240 + def terminate; end + + private + + # source://webrick//lib/webrick/utils.rb#158 + def watch; end + + # source://webrick//lib/webrick/utils.rb#193 + def watcher; end + + class << self + # Cancels the timeout handler +id+ + # + # source://webrick//lib/webrick/utils.rb#137 + def cancel(id); end + + # Registers a new timeout handler + # + # +time+:: Timeout in seconds + # +exception+:: Exception to raise when timeout elapsed + # + # source://webrick//lib/webrick/utils.rb#130 + def register(seconds, exception); end + + # source://webrick//lib/webrick/utils.rb#141 + def terminate; end + end +end diff --git a/sorbet/tapioca/require.rb b/sorbet/tapioca/require.rb index 7573e61e7a..7026a652ad 100644 --- a/sorbet/tapioca/require.rb +++ b/sorbet/tapioca/require.rb @@ -9,7 +9,9 @@ $LOAD_PATH.delete_if { |path| yarp_require_paths.include?(path) } if yarp_require_paths require "language_server-protocol" +require "json_rpc_handler" require "prism" +require "webrick" require "prism/visitor" require "mocha/minitest" require "rubocop/minitest/assert_offense" diff --git a/test/global_state_test.rb b/test/global_state_test.rb index 61214caa5e..a08f835e67 100644 --- a/test/global_state_test.rb +++ b/test/global_state_test.rb @@ -314,6 +314,75 @@ def test_saves_telemetry_machine_id assert_equal("test_machine_id", state.telemetry_machine_id) end + def test_detects_vscode_ruby_mcp + state = GlobalState.new + + # Stub the bin_rails_present method + state.stubs(:bin_rails_present).returns(false) + + stub_workspace_file_does_not_exist(".cursor/mcp.json") + stub_workspace_file_exists(".vscode/mcp.json") + File.stubs(:read).with("#{Dir.pwd}/.vscode/mcp.json").returns('{"servers":{"rubyMcp":{"command":"path"}}}') + + state.apply_options({}) + assert(state.uses_ruby_mcp) + end + + def test_detects_cursor_ruby_mcp + state = GlobalState.new + + # Stub the bin_rails_present method + state.stubs(:bin_rails_present).returns(false) + + stub_workspace_file_does_not_exist(".vscode/mcp.json") + stub_workspace_file_exists(".cursor/mcp.json") + File.stubs(:read).with("#{Dir.pwd}/.cursor/mcp.json").returns('{"mcpServers":{"rubyMcp":{"command":"path"}}}') + + state.apply_options({}) + assert(state.uses_ruby_mcp) + end + + def test_does_not_detect_ruby_mcp_when_no_files_exist + state = GlobalState.new + + # Stub the bin_rails_present method + state.stubs(:bin_rails_present).returns(false) + + stub_workspace_file_does_not_exist(".vscode/mcp.json") + stub_workspace_file_does_not_exist(".cursor/mcp.json") + + state.apply_options({}) + refute(state.uses_ruby_mcp) + end + + def test_does_not_detect_ruby_mcp_when_vscode_has_no_config + state = GlobalState.new + + # Stub the bin_rails_present method + state.stubs(:bin_rails_present).returns(false) + + stub_workspace_file_exists(".vscode/mcp.json") + stub_workspace_file_does_not_exist(".cursor/mcp.json") + File.stubs(:read).with("#{Dir.pwd}/.vscode/mcp.json").returns('{"servers":{"otherServer":{"command":"path"}}}') + + state.apply_options({}) + refute(state.uses_ruby_mcp) + end + + def test_does_not_detect_ruby_mcp_when_cursor_has_no_config + state = GlobalState.new + + # Stub the bin_rails_present method + state.stubs(:bin_rails_present).returns(false) + + stub_workspace_file_does_not_exist(".vscode/mcp.json") + stub_workspace_file_exists(".cursor/mcp.json") + File.stubs(:read).with("#{Dir.pwd}/.cursor/mcp.json").returns('{"mcpServers":{"otherServer":{"command":"path"}}}') + + state.apply_options({}) + refute(state.uses_ruby_mcp) + end + private def stub_direct_dependencies(dependencies) @@ -326,7 +395,11 @@ def stub_all_dependencies(*dependencies) end def stub_workspace_file_exists(path) - File.expects(:exist?).with("#{Dir.pwd}/#{path}").returns(true) + File.stubs(:exist?).with("#{Dir.pwd}/#{path}").returns(true) + end + + def stub_workspace_file_does_not_exist(path) + File.stubs(:exist?).with("#{Dir.pwd}/#{path}").returns(false) end end end diff --git a/test/mcp_server_test.rb b/test/mcp_server_test.rb new file mode 100644 index 0000000000..0746e6f452 --- /dev/null +++ b/test/mcp_server_test.rb @@ -0,0 +1,349 @@ +# typed: true +# frozen_string_literal: true + +require "test_helper" + +module RubyLsp + class MCPServerTest < Minitest::Test + def setup + @global_state = GlobalState.new + @server = MCPServer.new(@global_state) + @index = @global_state.index + end + + def teardown + # Avoid printing closing message + capture_io do + @server.stop + end + end + + def test_handle_get_classes_and_modules_no_query + # Index some sample classes and modules + @index.index_single(URI("file:///fake.rb"), <<~RUBY) + class Foo; end + module Bar; end + RUBY + + result = @server.send(:handle_get_classes_and_modules, nil) + expected_yaml = [{ name: "Foo", type: "class" }, { name: "Bar", type: "module" }].to_yaml + expected_result = [{ type: "text", text: expected_yaml }] + + assert_equal(expected_result, result) + end + + def test_handle_get_classes_and_modules_with_query + # Index some sample classes and modules + @index.index_single(URI("file:///fake.rb"), <<~RUBY) + class FooClass; end + module FooModule; end + class AnotherClass; end + RUBY + + result = @server.send(:handle_get_classes_and_modules, "Foo") + expected_yaml = [{ name: "FooClass", type: "class" }, { name: "FooModule", type: "module" }].to_yaml + expected_result = [{ type: "text", text: expected_yaml }] + + assert_equal(expected_result, result) + end + + def test_handle_get_classes_and_modules_too_many_results + original_max_classes = MCPServer::MAX_CLASSES_TO_RETURN + MCPServer.const_set(:MAX_CLASSES_TO_RETURN, 1) + + # Index more classes than the limit + @index.index_single(URI("file:///fake.rb"), <<~RUBY) + class Class1; end + class Class2; end + RUBY + + result = @server.send(:handle_get_classes_and_modules, nil) + + assert_equal(2, result.size) + assert_equal("text", result[0][:type]) + assert_equal( + "Too many classes and modules to return, please narrow down your request with a query.", + result[0][:text], + ) + assert_equal("text", result[1][:type]) + # Check that only the first MAX_CLASSES_TO_RETURN are included + expected_yaml = [{ name: "Class1", type: "class" }].to_yaml + assert_equal(expected_yaml, result[1][:text]) + ensure + MCPServer.const_set(:MAX_CLASSES_TO_RETURN, original_max_classes) + end + + def test_handle_read_ruby_files_single_file + file_path = File.join(Dir.pwd, "test_read.rb") + file_content = "# Test content" + File.write(file_path, file_content) + + begin + result = @server.send(:handle_read_ruby_files, ["file://#{file_path}"]) + expected_yaml = { file_path: file_path, file_content: file_content }.to_yaml + expected_result = [{ type: "text", text: expected_yaml }] + + assert_equal(expected_result, result) + ensure + File.delete(file_path) if File.exist?(file_path) + end + end + + def test_handle_read_ruby_files_multiple_files + file_path1 = File.join(Dir.pwd, "test_read1.rb") + file_content1 = "# Test content 1" + File.write(file_path1, file_content1) + + file_path2 = File.join(Dir.pwd, "test_read2.rb") + file_content2 = "# Test content 2" + File.write(file_path2, file_content2) + + begin + result = @server.send(:handle_read_ruby_files, ["file://#{file_path1}", "file://#{file_path2}"]) + + expected_yaml1 = { file_path: file_path1, file_content: file_content1 }.to_yaml + expected_yaml2 = { file_path: file_path2, file_content: file_content2 }.to_yaml + expected_result = [ + { type: "text", text: expected_yaml1 }, + { type: "text", text: expected_yaml2 }, + ] + + assert_equal(expected_result, result) + ensure + File.delete(file_path1) if File.exist?(file_path1) + File.delete(file_path2) if File.exist?(file_path2) + end + end + + def test_handle_read_ruby_files_non_existent_file + non_existent_path = File.join(Dir.pwd, "non_existent.rb") + result = @server.send(:handle_read_ruby_files, ["file://#{non_existent_path}"]) + + expected_yaml = { file_path: non_existent_path, error: "File not found" }.to_yaml + expected_result = [{ type: "text", text: expected_yaml }] + + assert_equal(expected_result, result) + end + + def test_handle_get_methods_details_instance_method + uri = URI("file:///fake_instance.rb") + @index.index_single(uri, <<~RUBY) + class MyClass + # Method comment + def my_method(param1) + end + end + RUBY + + result = @server.send(:handle_get_methods_details, ["MyClass#my_method"]) + entry = @index.resolve_method("my_method", "MyClass").first + + # Parse actual result + result_yaml = Psych.unsafe_load(result[0][:text]) + + # Define expected simple values + expected_receiver = "MyClass" + expected_method = "my_method" + # Define expected complex part (entry_details) as a hash + expected_details_hash = [ + { + uri: entry.uri, + visibility: entry.visibility, + comments: entry.comments.is_a?(Array) ? entry.comments.join("\n") : entry.comments, + parameters: entry.decorated_parameters, + owner: "MyClass", + }, + ] + + # Compare simple fields + assert_equal(expected_receiver, result_yaml[:receiver]) + assert_equal(expected_method, result_yaml[:method]) + # Compare the entry_details part by converting both back to YAML strings + assert_equal(expected_details_hash.to_yaml, result_yaml[:entry_details].to_yaml) + end + + def test_handle_get_methods_details_singleton_method + uri = URI("file:///fake_singleton.rb") + @index.index_single(uri, <<~RUBY) + class MyClass + # Singleton method comment + def self.my_singleton_method + end + end + RUBY + + singleton_class_name = "MyClass::" + result = @server.send(:handle_get_methods_details, ["MyClass.my_singleton_method"]) + entry = @index.resolve_method("my_singleton_method", singleton_class_name).first + + # Parse actual result + result_yaml = Psych.unsafe_load(result[0][:text]) + + # Define expected simple values + expected_receiver = "MyClass" + expected_method = "my_singleton_method" + # Define expected complex part (entry_details) as a hash + expected_details_hash = [ + { + uri: entry.uri, + visibility: entry.visibility, + comments: entry.comments.is_a?(Array) ? entry.comments.join("\n") : entry.comments, + parameters: entry.decorated_parameters, + owner: singleton_class_name, + }, + ] + + # Compare simple fields + assert_equal(expected_receiver, result_yaml[:receiver]) + assert_equal(expected_method, result_yaml[:method]) + # Compare the entry_details part by converting both back to YAML strings + assert_equal(expected_details_hash.to_yaml, result_yaml[:entry_details].to_yaml) + end + + def test_handle_get_methods_details_method_not_found + @index.index_single(URI("file:///fake_not_found.rb"), "class MyClass; end") + result = @server.send(:handle_get_methods_details, ["MyClass#non_existent_method"]) + assert_empty(result) + end + + def test_handle_get_methods_details_receiver_not_found + result = @server.send(:handle_get_methods_details, ["NonExistentClass#method"]) + assert_empty(result) + end + + def test_handle_get_class_module_details_class + uri = URI("file:///fake_class_details.rb") + @index.index_single(uri, <<~RUBY) + # Class Comment + class MyDetailedClass + def instance_method; end + def self.singleton_method; end + end + RUBY + + result = @server.send(:handle_get_class_module_details, ["MyDetailedClass"]) + entry = @index.resolve("MyDetailedClass", []).first + + expected_text = { + name: "MyDetailedClass", + nestings: [], + type: "class", + ancestors: ["MyDetailedClass"], + methods: ["instance_method"], + uris: [entry.uri], + documentation: "__PLACEHOLDER__", + } + + result_yaml = Psych.unsafe_load(result[0][:text]) + actual_documentation = result_yaml.delete(:documentation) + expected_text.delete(:documentation) + + assert_equal(1, result.size) + assert_equal("text", result[0][:type]) + # Compare the hash without documentation + assert_equal(expected_text, result_yaml) + # Assert documentation content separately + assert_includes(actual_documentation, "Class Comment") + assert_includes(actual_documentation, "**Definitions**: [fake_class_details.rb]") + end + + def test_handle_get_class_module_details_module + uri = URI("file:///fake_module_details.rb") + @index.index_single(uri, <<~RUBY) + # Module Comment + module MyDetailedModule + def instance_method_in_module; end + end + RUBY + + result = @server.send(:handle_get_class_module_details, ["MyDetailedModule"]) + entry = @index.resolve("MyDetailedModule", []).first + + expected_text = { + name: "MyDetailedModule", + nestings: [], + type: "module", + ancestors: ["MyDetailedModule"], + methods: ["instance_method_in_module"], + uris: [entry.uri], + documentation: "__PLACEHOLDER__", + } + + result_yaml = Psych.unsafe_load(result[0][:text]) + actual_documentation = result_yaml.delete(:documentation) + expected_text.delete(:documentation) + + assert_equal(1, result.size) + assert_equal("text", result[0][:type]) + # Compare the hash without documentation + assert_equal(expected_text, result_yaml) + # Assert documentation content separately + assert_includes(actual_documentation, "Module Comment") + assert_includes(actual_documentation, "**Definitions**: [fake_module_details.rb]") + end + + def test_handle_get_class_module_details_nested + uri = URI("file:///fake_nested_details.rb") + @index.index_single(uri, <<~RUBY) + module Outer + # Nested Class Comment + class InnerClass + def inner_method; end + end + end + RUBY + + result = @server.send(:handle_get_class_module_details, ["Outer::InnerClass"]) + entry = @index.resolve("InnerClass", ["Outer"]).first + + expected_text = { + name: "Outer::InnerClass", + nestings: ["Outer"], + type: "class", + ancestors: ["Outer::InnerClass"], + methods: ["inner_method"], + uris: [entry.uri], + documentation: "__PLACEHOLDER__", + } + + result_yaml = Psych.unsafe_load(result[0][:text]) + actual_documentation = result_yaml.delete(:documentation) + expected_text.delete(:documentation) + + assert_equal(1, result.size) + assert_equal("text", result[0][:type]) + # Compare the hash without documentation + assert_equal(expected_text, result_yaml) + # Assert documentation content separately + assert_includes(actual_documentation, "Nested Class Comment") + assert_includes(actual_documentation, "**Definitions**: [fake_nested_details.rb]") + end + + def test_handle_get_class_module_details_not_found + result = @server.send(:handle_get_class_module_details, ["NonExistentThing"]) + + expected_text = { + name: "NonExistentThing", + nestings: [], + type: "unknown", + ancestors: [], + methods: [], + uris: [], + documentation: "__PLACEHOLDER__", + } + + result_yaml = Psych.unsafe_load(result[0][:text]) + actual_documentation = result_yaml.delete(:documentation) + expected_text.delete(:documentation) + + assert_equal(1, result.size) + assert_equal("text", result[0][:type]) + # Compare the hash without documentation + assert_equal(expected_text, result_yaml) + # Assert documentation content separately (structure but no specific comment) + assert_includes(actual_documentation, "**Definitions**: ") + # Ensure no accidental comment appeared + refute_match(/^[A-Za-z]/, actual_documentation.split("**Definitions**: ").last.strip) + end + end +end diff --git a/vscode/ruby-mcp-bridge b/vscode/ruby-mcp-bridge new file mode 100755 index 0000000000..88a6fcb3e8 --- /dev/null +++ b/vscode/ruby-mcp-bridge @@ -0,0 +1,41 @@ +#!/bin/bash + +# Determine script location and workspace path +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKSPACE_RUBY_LSP_DIR="$(dirname "$SCRIPT_DIR")/.ruby-lsp" +PORT_FILE="$WORKSPACE_RUBY_LSP_DIR/mcp-port" +LOG_PATH="$WORKSPACE_RUBY_LSP_DIR/mcp-bridge.log" + +# Ensure log directory exists +mkdir -p "$WORKSPACE_RUBY_LSP_DIR" + +# Check if port file exists +if [ ! -f "$PORT_FILE" ]; then + echo "Error: MCP port file not found at $PORT_FILE" >&2 + echo "MCP server may not be running." >&2 + exit 1 +fi + +# Read port from file +PORT=$(cat "$PORT_FILE") + +echo "Bridge started using port $PORT" >> "$LOG_PATH" + +while IFS= read -r line; do + echo "================================" >> "$LOG_PATH" + echo "Input JSON: $line" >> "$LOG_PATH" + + # Send HTTP request to the WEBrick server + response=$(curl -s -X POST -H "Content-Type: application/json" \ + -H "Content-Length: ${#line}" \ + --data "$line" \ + "http://127.0.0.1:$PORT/mcp" 2>$LOG_PATH) || { + echo "HTTP request failed" >> "$LOG_PATH" + continue + } + + echo "--------------------------------" >> "$LOG_PATH" + echo "Response JSON: $response" >> "$LOG_PATH" + + echo "$response" +done diff --git a/vscode/src/workspace.ts b/vscode/src/workspace.ts index 36bd07b039..9478373a61 100644 --- a/vscode/src/workspace.ts +++ b/vscode/src/workspace.ts @@ -172,6 +172,7 @@ export class Workspace implements WorkspaceInterface { title: "Initializing Ruby LSP", }, async () => { + await this.copyRubyMcpBridge(); await this.lspClient!.start(); await this.lspClient!.afterStart(); }, @@ -442,6 +443,19 @@ export class Workspace implements WorkspaceInterface { ); } + private async copyRubyMcpBridge() { + const targetURI = vscode.Uri.joinPath( + this.workspaceFolder.uri, + ".ruby-lsp", + "ruby-mcp-bridge", + ); + await vscode.workspace.fs.copy( + vscode.Uri.joinPath(this.context.extensionUri, "ruby-mcp-bridge"), + targetURI, + { overwrite: true }, + ); + } + private async fileContentsSha(uri: vscode.Uri): Promise { let fileContents; From 804b1c55a11f053d14eaccff2a0c901608a3aa13 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Sat, 19 Apr 2025 12:34:13 +0900 Subject: [PATCH 02/13] Remove read_ruby_file tool as it can be abused --- lib/ruby_lsp/mcp_server.rb | 56 -------------------------------------- test/mcp_server_test.rb | 52 ----------------------------------- 2 files changed, 108 deletions(-) diff --git a/lib/ruby_lsp/mcp_server.rb b/lib/ruby_lsp/mcp_server.rb index 708b83ac6e..33d8977662 100644 --- a/lib/ruby_lsp/mcp_server.rb +++ b/lib/ruby_lsp/mcp_server.rb @@ -167,24 +167,6 @@ def process_jsonrpc_request(json) }, }, }, - { - # This may be redundant to some clients if they can access terminal to cat the files - # but it's useful for some clients that don't have that capability - name: "read_ruby_files", - description: <<~DESCRIPTION, - Read the contents of the given Ruby files, including files from dependencies. - DESCRIPTION - inputSchema: { - type: "object", - properties: { - file_uris: { - type: "array", - items: { type: "string" }, - }, - }, - required: ["file_uris"], - }, - }, { name: "get_methods_details", description: <<~DESCRIPTION, @@ -245,8 +227,6 @@ def process_jsonrpc_request(json) contents = case params[:name] when "get_classes_and_modules" handle_get_classes_and_modules(params.dig(:arguments, :query)) - when "read_ruby_files" - handle_read_ruby_files(params.dig(:arguments, :file_uris)) when "get_methods_details" handle_get_methods_details(params.dig(:arguments, :signatures)) when "get_class_module_details" @@ -316,42 +296,6 @@ def handle_get_classes_and_modules(query) end end - #: (Array[String]) -> Array[Hash[Symbol, untyped]] - def handle_read_ruby_files(file_uris) - file_uris.map do |file_uri| - file_uri_obj = URI(file_uri) - file_path = file_uri_obj.path - next unless file_path - - begin - file_content = File.read(file_path) - { - type: "text", - text: { - file_path: file_path, - file_content: file_content, - }.to_yaml, - } - rescue Errno::ENOENT - { - type: "text", - text: { - file_path: file_path, - error: "File not found", - }.to_yaml, - } - rescue => e - { - type: "text", - text: { - file_path: file_path, - error: "Error reading file: #{e.message}", - }.to_yaml, - } - end - end.compact - end - #: (Array[String]) -> Array[Hash[Symbol, untyped]] def handle_get_methods_details(signatures) signatures.map do |signature| diff --git a/test/mcp_server_test.rb b/test/mcp_server_test.rb index 0746e6f452..8b4fdbce57 100644 --- a/test/mcp_server_test.rb +++ b/test/mcp_server_test.rb @@ -73,58 +73,6 @@ class Class2; end MCPServer.const_set(:MAX_CLASSES_TO_RETURN, original_max_classes) end - def test_handle_read_ruby_files_single_file - file_path = File.join(Dir.pwd, "test_read.rb") - file_content = "# Test content" - File.write(file_path, file_content) - - begin - result = @server.send(:handle_read_ruby_files, ["file://#{file_path}"]) - expected_yaml = { file_path: file_path, file_content: file_content }.to_yaml - expected_result = [{ type: "text", text: expected_yaml }] - - assert_equal(expected_result, result) - ensure - File.delete(file_path) if File.exist?(file_path) - end - end - - def test_handle_read_ruby_files_multiple_files - file_path1 = File.join(Dir.pwd, "test_read1.rb") - file_content1 = "# Test content 1" - File.write(file_path1, file_content1) - - file_path2 = File.join(Dir.pwd, "test_read2.rb") - file_content2 = "# Test content 2" - File.write(file_path2, file_content2) - - begin - result = @server.send(:handle_read_ruby_files, ["file://#{file_path1}", "file://#{file_path2}"]) - - expected_yaml1 = { file_path: file_path1, file_content: file_content1 }.to_yaml - expected_yaml2 = { file_path: file_path2, file_content: file_content2 }.to_yaml - expected_result = [ - { type: "text", text: expected_yaml1 }, - { type: "text", text: expected_yaml2 }, - ] - - assert_equal(expected_result, result) - ensure - File.delete(file_path1) if File.exist?(file_path1) - File.delete(file_path2) if File.exist?(file_path2) - end - end - - def test_handle_read_ruby_files_non_existent_file - non_existent_path = File.join(Dir.pwd, "non_existent.rb") - result = @server.send(:handle_read_ruby_files, ["file://#{non_existent_path}"]) - - expected_yaml = { file_path: non_existent_path, error: "File not found" }.to_yaml - expected_result = [{ type: "text", text: expected_yaml }] - - assert_equal(expected_result, result) - end - def test_handle_get_methods_details_instance_method uri = URI("file:///fake_instance.rb") @index.index_single(uri, <<~RUBY) From e159730cb5fe40dc9a15f872c6d26f6d34927f10 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 28 May 2025 15:11:54 -0400 Subject: [PATCH 03/13] Avoid starting MCP server on CI --- lib/ruby_lsp/server.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ruby_lsp/server.rb b/lib/ruby_lsp/server.rb index ba6bacbf25..46dc74fb49 100644 --- a/lib/ruby_lsp/server.rb +++ b/lib/ruby_lsp/server.rb @@ -378,7 +378,7 @@ def run_initialized #: -> void def start_mcp_server - return unless @global_state.uses_ruby_mcp + return if ENV["CI"] || !@global_state.uses_ruby_mcp @mcp_server = T.let(MCPServer.new(@global_state), T.nilable(MCPServer)) T.must(@mcp_server).start From d1ef6d5d397d49ae9253c501cf926e7b62c59cbd Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 28 May 2025 16:36:59 -0400 Subject: [PATCH 04/13] Ensure MCP server finishes cleanup --- lib/ruby_lsp/mcp_server.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ruby_lsp/mcp_server.rb b/lib/ruby_lsp/mcp_server.rb index 33d8977662..85f2607091 100644 --- a/lib/ruby_lsp/mcp_server.rb +++ b/lib/ruby_lsp/mcp_server.rb @@ -67,7 +67,7 @@ def start def stop puts "[MCP] Stopping server" @server.shutdown - + ensure # Clean up port file lsp_dir = File.join(@workspace_path, ".ruby-lsp") port_file = File.join(lsp_dir, "mcp-port") From 095b6964f8e5101879abe9183288ff5939863056 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 28 May 2025 16:35:53 -0400 Subject: [PATCH 05/13] Extract tool implementation into Tool classes --- lib/ruby_lsp/mcp/tool.rb | 292 +++++++++++++++++++++++++++++++++++++ lib/ruby_lsp/mcp_server.rb | 215 ++------------------------- test/mcp_server_test.rb | 67 +++++---- 3 files changed, 344 insertions(+), 230 deletions(-) create mode 100644 lib/ruby_lsp/mcp/tool.rb diff --git a/lib/ruby_lsp/mcp/tool.rb b/lib/ruby_lsp/mcp/tool.rb new file mode 100644 index 0000000000..bffed9a3b9 --- /dev/null +++ b/lib/ruby_lsp/mcp/tool.rb @@ -0,0 +1,292 @@ +# typed: strict +# frozen_string_literal: true + +require "ruby_lsp/requests/support/common" + +module RubyLsp + module MCP + # @abstract + class Tool + include RubyLsp::Requests::Support::Common + + MAX_CLASSES_TO_RETURN = 5000 + + @tools = {} #: Hash[String, singleton(Tool)] + + #: (RubyIndexer::Index) -> void + def initialize(index) + @index = index #: RubyIndexer::Index + end + + # @abstract + #: (Hash[String, untyped]) -> Array[Hash[Symbol, untyped]] + def call(arguments); end + + class << self + #: Hash[String, singleton(Tool)] + attr_reader :tools + + #: (singleton(Tool)) -> void + def register(tool_class) + tools[tool_class.name] = tool_class + end + + #: (String) -> singleton(Tool)? + def get(name) + tools[name] + end + + # @abstract + #: -> String + def name; end + + # @abstract + #: -> String + def description; end + + # @abstract + #: -> Hash[Symbol, untyped] + def input_schema; end + end + end + + class GetClassModuleDetails < Tool + class << self + # @override + #: -> String + def name + "get_class_module_details" + end + + # @override + #: -> String + def description + <<~DESCRIPTION + Show the details of the given classes/modules that are available in the current project and + its dependencies. + - Comments + - Definition location + - Methods + - Ancestors + + Use `get_methods_details` tool to get the details of specific methods of a class/module. + DESCRIPTION + end + + # @override + #: -> Hash[Symbol, untyped] + def input_schema + { + type: "object", + properties: { + fully_qualified_names: { type: "array", items: { type: "string" } }, + }, + } + end + end + + # @override + #: (Hash[String, untyped]) -> Array[Hash[Symbol, untyped]] + def call(arguments) + fully_qualified_names = arguments["fully_qualified_names"] + fully_qualified_names.map do |fully_qualified_name| + *nestings, name = fully_qualified_name.delete_prefix("::").split("::") + entries = @index.resolve(name, nestings) || [] + + begin + ancestors = @index.linearized_ancestors_of(fully_qualified_name) + methods = @index.method_completion_candidates(nil, fully_qualified_name) + rescue RubyIndexer::Index::NonExistingNamespaceError + # If the namespace doesn't exist, we can't find ancestors or methods + ancestors = [] + methods = [] + end + + type = case entries.first + when RubyIndexer::Entry::Class + "class" + when RubyIndexer::Entry::Module + "module" + else + "unknown" + end + + { + type: "text", + text: { + name: fully_qualified_name, + nestings: nestings, + type: type, + ancestors: ancestors, + methods: methods.map(&:name), + uris: entries.map(&:uri), + documentation: markdown_from_index_entries(name, entries), + }.to_yaml, + } + end + end + end + + class GetMethodsDetails < Tool + class << self + # @override + #: -> String + def name + "get_methods_details" + end + + # @override + #: -> String + def description + <<~DESCRIPTION + Show the details of the given methods. + Use the following format for the signatures: + - Class#method + - Module#method + - Class.singleton_method + - Module.singleton_method + + Details include: + - Comments + - Definition location + - Visibility + - Parameters + - Owner + + DESCRIPTION + end + + # @override + #: -> Hash[Symbol, untyped] + def input_schema + { + type: "object", + properties: { + signatures: { type: "array", items: { type: "string" } }, + }, + } + end + end + + # @override + #: (Hash[String, untyped]) -> Array[Hash[Symbol, untyped]] + def call(arguments) + signatures = arguments["signatures"] || [] + signatures.map do |signature| + entries = nil + receiver = nil + method = nil + + if signature.include?("#") + receiver, method = signature.split("#") + entries = @index.resolve_method(method, receiver) + elsif signature.include?(".") + receiver, method = signature.split(".") + singleton_class = @index.existing_or_new_singleton_class(receiver) + entries = @index.resolve_method(method, singleton_class.name) + end + + next if entries.nil? + + entry_details = entries.map do |entry| + { + uri: entry.uri, + visibility: entry.visibility, + comments: entry.comments, + parameters: entry.decorated_parameters, + owner: entry.owner&.name, + } + end + + { + type: "text", + text: { + receiver: receiver, + method: method, + entry_details: entry_details, + }.to_yaml, + } + end.compact + end + end + + class GetClassesAndModules < Tool + class << self + # @override + #: -> String + def name + "get_classes_and_modules" + end + + # @override + #: -> String + def description + <<~DESCRIPTION + Show all the indexed classes and modules in the current project and its dependencies when no query is provided. + When a query is provided, it'll return a list of classes and modules that match the query. + Doesn't support pagination and will return all classes and modules. + Stops after #{Tool::MAX_CLASSES_TO_RETURN} classes and modules. + DESCRIPTION + end + + # @override + #: -> Hash[Symbol, untyped] + def input_schema + { + type: "object", + properties: { + query: { + type: "string", + description: "A query to filter the classes and modules", + }, + }, + } + end + end + + # @override + #: (Hash[String, untyped]) -> Array[Hash[Symbol, untyped]] + def call(arguments) + query = arguments["query"] + class_names = @index.fuzzy_search(query).map do |entry| + case entry + when RubyIndexer::Entry::Class + { + name: entry.name, + type: "class", + } + when RubyIndexer::Entry::Module + { + name: entry.name, + type: "module", + } + end + end.compact.uniq + + if class_names.size > MAX_CLASSES_TO_RETURN + [ + { + type: "text", + text: "Too many classes and modules to return, please narrow down your request with a query.", + }, + { + type: "text", + text: class_names.first(MAX_CLASSES_TO_RETURN).to_yaml, + }, + ] + else + [ + { + type: "text", + text: class_names.to_yaml, + }, + ] + end + end + end + + Tool.register(GetClassesAndModules) + Tool.register(GetMethodsDetails) + Tool.register(GetClassModuleDetails) + end +end diff --git a/lib/ruby_lsp/mcp_server.rb b/lib/ruby_lsp/mcp_server.rb index 85f2607091..df3812af01 100644 --- a/lib/ruby_lsp/mcp_server.rb +++ b/lib/ruby_lsp/mcp_server.rb @@ -1,15 +1,10 @@ # typed: strict # frozen_string_literal: true -# TODO: Extract MCP requests and load them separately -require "ruby_lsp/requests/support/common" +require "ruby_lsp/mcp/tool" module RubyLsp class MCPServer - include RubyLsp::Requests::Support::Common - - MAX_CLASSES_TO_RETURN = 5000 - class << self # Find an available TCP port #: -> Integer @@ -148,92 +143,25 @@ def process_jsonrpc_request(json) when "tools/list" ->(_) do { - tools: [ - { - name: "get_classes_and_modules", - description: <<~DESCRIPTION, - Show all the indexed classes and modules in the current project and its dependencies when no query is provided. - When a query is provided, it'll return a list of classes and modules that match the query. - Doesn't support pagination and will return all classes and modules. - Stops after #{MAX_CLASSES_TO_RETURN} classes and modules. - DESCRIPTION - inputSchema: { - type: "object", - properties: { - query: { - type: "string", - description: "A query to filter the classes and modules", - }, - }, - }, - }, - { - name: "get_methods_details", - description: <<~DESCRIPTION, - Show the details of the given methods. - Use the following format for the signatures: - - Class#method - - Module#method - - Class.singleton_method - - Module.singleton_method - - Details include: - - Comments - - Definition location - - Visibility - - Parameters - - Owner - DESCRIPTION - inputSchema: { - type: "object", - properties: { - signatures: { - type: "array", - items: { type: "string" }, - }, - }, - required: ["signatures"], - }, - }, + tools: RubyLsp::MCP::Tool.tools.map do |tool_name, tool_class| { - name: "get_class_module_details", - description: <<~DESCRIPTION, - Show the details of the given classes/modules that are available in the current project and - its dependencies. - - Comments - - Definition location - - Methods - - Ancestors - - Use `get_methods_details` tool to get the details of specific methods of a class/module. - DESCRIPTION - inputSchema: { - type: "object", - properties: { - fully_qualified_names: { - type: "array", - items: { type: "string" }, - }, - }, - required: ["fully_qualified_names"], - }, - }, - ], + name: tool_name, + description: tool_class.description.dump, # avoid newlines in the description + inputSchema: tool_class.input_schema, + } + end, } end when "tools/call" ->(params) { puts "[MCP] Received tools/call request: #{params.inspect}" - contents = case params[:name] - when "get_classes_and_modules" - handle_get_classes_and_modules(params.dig(:arguments, :query)) - when "get_methods_details" - handle_get_methods_details(params.dig(:arguments, :signatures)) - when "get_class_module_details" - handle_get_class_module_details(params.dig(:arguments, :fully_qualified_names)) - end + tool_name = params[:name] + tool_class = RubyLsp::MCP::Tool.get(tool_name) - generate_response(contents) if contents + if tool_class + contents = tool_class.new(@index).call(params[:arguments] || {}) + generate_response(contents) + end } end end @@ -256,122 +184,5 @@ def generate_response(contents) } end end - - # Tool implementations - #: (String?) -> Array[Hash[Symbol, untyped]] - def handle_get_classes_and_modules(query) - class_names = @index.fuzzy_search(query).map do |entry| - case entry - when RubyIndexer::Entry::Class - { - name: entry.name, - type: "class", - } - when RubyIndexer::Entry::Module - { - name: entry.name, - type: "module", - } - end - end.compact.uniq - - if class_names.size > MAX_CLASSES_TO_RETURN - [ - { - type: "text", - text: "Too many classes and modules to return, please narrow down your request with a query.", - }, - { - type: "text", - text: class_names.first(MAX_CLASSES_TO_RETURN).to_yaml, - }, - ] - else - [ - { - type: "text", - text: class_names.to_yaml, - }, - ] - end - end - - #: (Array[String]) -> Array[Hash[Symbol, untyped]] - def handle_get_methods_details(signatures) - signatures.map do |signature| - entries = nil - receiver = nil - method = nil - - if signature.include?("#") - receiver, method = signature.split("#") - entries = @index.resolve_method(T.must(method), T.must(receiver)) - elsif signature.include?(".") - receiver, method = signature.split(".") - singleton_class = @index.existing_or_new_singleton_class(T.must(receiver)) - entries = @index.resolve_method(T.must(method), singleton_class.name) - end - - next if entries.nil? - - entry_details = entries.map do |entry| - { - uri: entry.uri, - visibility: entry.visibility, - comments: entry.comments, - parameters: entry.decorated_parameters, - owner: entry.owner&.name, - } - end - - { - type: "text", - text: { - receiver: receiver, - method: method, - entry_details: entry_details, - }.to_yaml, - } - end.compact - end - - #: (Array[String]) -> Array[Hash[Symbol, untyped]] - def handle_get_class_module_details(fully_qualified_names) - fully_qualified_names.map do |fully_qualified_name| - *nestings, name = fully_qualified_name.delete_prefix("::").split("::") - entries = @index.resolve(T.must(name), nestings) || [] - - begin - ancestors = @index.linearized_ancestors_of(fully_qualified_name) - methods = @index.method_completion_candidates(nil, fully_qualified_name) - rescue RubyIndexer::Index::NonExistingNamespaceError - # If the namespace doesn't exist, we can't find ancestors or methods - ancestors = [] - methods = [] - end - - type = case entries.first - when RubyIndexer::Entry::Class - "class" - when RubyIndexer::Entry::Module - "module" - else - "unknown" - end - - { - type: "text", - text: { - name: fully_qualified_name, - nestings: nestings, - type: type, - ancestors: ancestors, - methods: methods.map(&:name), - uris: entries.map(&:uri), - documentation: markdown_from_index_entries(T.must(name), entries), - }.to_yaml, - } - end - end end end diff --git a/test/mcp_server_test.rb b/test/mcp_server_test.rb index 8b4fdbce57..65f8bf48e5 100644 --- a/test/mcp_server_test.rb +++ b/test/mcp_server_test.rb @@ -25,7 +25,8 @@ class Foo; end module Bar; end RUBY - result = @server.send(:handle_get_classes_and_modules, nil) + tool = RubyLsp::MCP::GetClassesAndModules.new(@index) + result = tool.call({}) expected_yaml = [{ name: "Foo", type: "class" }, { name: "Bar", type: "module" }].to_yaml expected_result = [{ type: "text", text: expected_yaml }] @@ -40,7 +41,8 @@ module FooModule; end class AnotherClass; end RUBY - result = @server.send(:handle_get_classes_and_modules, "Foo") + tool = RubyLsp::MCP::GetClassesAndModules.new(@index) + result = tool.call({ "query" => "Foo" }) expected_yaml = [{ name: "FooClass", type: "class" }, { name: "FooModule", type: "module" }].to_yaml expected_result = [{ type: "text", text: expected_yaml }] @@ -48,8 +50,8 @@ class AnotherClass; end end def test_handle_get_classes_and_modules_too_many_results - original_max_classes = MCPServer::MAX_CLASSES_TO_RETURN - MCPServer.const_set(:MAX_CLASSES_TO_RETURN, 1) + original_max_classes = RubyLsp::MCP::Tool::MAX_CLASSES_TO_RETURN + RubyLsp::MCP::Tool.const_set(:MAX_CLASSES_TO_RETURN, 1) # Index more classes than the limit @index.index_single(URI("file:///fake.rb"), <<~RUBY) @@ -57,20 +59,21 @@ class Class1; end class Class2; end RUBY - result = @server.send(:handle_get_classes_and_modules, nil) + tool = RubyLsp::MCP::GetClassesAndModules.new(@index) + result = tool.call({}) assert_equal(2, result.size) - assert_equal("text", result[0][:type]) + assert_equal("text", result.dig(0, :type)) assert_equal( "Too many classes and modules to return, please narrow down your request with a query.", - result[0][:text], + result.dig(0, :text), ) - assert_equal("text", result[1][:type]) + assert_equal("text", result.dig(1, :type)) # Check that only the first MAX_CLASSES_TO_RETURN are included expected_yaml = [{ name: "Class1", type: "class" }].to_yaml - assert_equal(expected_yaml, result[1][:text]) + assert_equal(expected_yaml, result.dig(1, :text)) ensure - MCPServer.const_set(:MAX_CLASSES_TO_RETURN, original_max_classes) + RubyLsp::MCP::Tool.const_set(:MAX_CLASSES_TO_RETURN, original_max_classes) end def test_handle_get_methods_details_instance_method @@ -83,11 +86,12 @@ def my_method(param1) end RUBY - result = @server.send(:handle_get_methods_details, ["MyClass#my_method"]) + tool = RubyLsp::MCP::GetMethodsDetails.new(@index) + result = tool.call({ "signatures" => ["MyClass#my_method"] }) entry = @index.resolve_method("my_method", "MyClass").first # Parse actual result - result_yaml = Psych.unsafe_load(result[0][:text]) + result_yaml = Psych.unsafe_load(result.dig(0, :text)) # Define expected simple values expected_receiver = "MyClass" @@ -121,11 +125,12 @@ def self.my_singleton_method RUBY singleton_class_name = "MyClass::" - result = @server.send(:handle_get_methods_details, ["MyClass.my_singleton_method"]) + tool = RubyLsp::MCP::GetMethodsDetails.new(@index) + result = tool.call({ "signatures" => ["MyClass.my_singleton_method"] }) entry = @index.resolve_method("my_singleton_method", singleton_class_name).first # Parse actual result - result_yaml = Psych.unsafe_load(result[0][:text]) + result_yaml = Psych.unsafe_load(result.dig(0, :text)) # Define expected simple values expected_receiver = "MyClass" @@ -150,12 +155,14 @@ def self.my_singleton_method def test_handle_get_methods_details_method_not_found @index.index_single(URI("file:///fake_not_found.rb"), "class MyClass; end") - result = @server.send(:handle_get_methods_details, ["MyClass#non_existent_method"]) + tool = RubyLsp::MCP::GetMethodsDetails.new(@index) + result = tool.call({ "signatures" => ["MyClass#non_existent_method"] }) assert_empty(result) end def test_handle_get_methods_details_receiver_not_found - result = @server.send(:handle_get_methods_details, ["NonExistentClass#method"]) + tool = RubyLsp::MCP::GetMethodsDetails.new(@index) + result = tool.call({ "signatures" => ["NonExistentClass#method"] }) assert_empty(result) end @@ -169,7 +176,8 @@ def self.singleton_method; end end RUBY - result = @server.send(:handle_get_class_module_details, ["MyDetailedClass"]) + tool = RubyLsp::MCP::GetClassModuleDetails.new(@index) + result = tool.call({ "fully_qualified_names" => ["MyDetailedClass"] }) entry = @index.resolve("MyDetailedClass", []).first expected_text = { @@ -182,12 +190,12 @@ def self.singleton_method; end documentation: "__PLACEHOLDER__", } - result_yaml = Psych.unsafe_load(result[0][:text]) + result_yaml = Psych.unsafe_load(result.dig(0, :text)) actual_documentation = result_yaml.delete(:documentation) expected_text.delete(:documentation) assert_equal(1, result.size) - assert_equal("text", result[0][:type]) + assert_equal("text", result.dig(0, :type)) # Compare the hash without documentation assert_equal(expected_text, result_yaml) # Assert documentation content separately @@ -204,7 +212,8 @@ def instance_method_in_module; end end RUBY - result = @server.send(:handle_get_class_module_details, ["MyDetailedModule"]) + tool = RubyLsp::MCP::GetClassModuleDetails.new(@index) + result = tool.call({ "fully_qualified_names" => ["MyDetailedModule"] }) entry = @index.resolve("MyDetailedModule", []).first expected_text = { @@ -217,12 +226,12 @@ def instance_method_in_module; end documentation: "__PLACEHOLDER__", } - result_yaml = Psych.unsafe_load(result[0][:text]) + result_yaml = Psych.unsafe_load(result.dig(0, :text)) actual_documentation = result_yaml.delete(:documentation) expected_text.delete(:documentation) assert_equal(1, result.size) - assert_equal("text", result[0][:type]) + assert_equal("text", result.dig(0, :type)) # Compare the hash without documentation assert_equal(expected_text, result_yaml) # Assert documentation content separately @@ -241,7 +250,8 @@ def inner_method; end end RUBY - result = @server.send(:handle_get_class_module_details, ["Outer::InnerClass"]) + tool = RubyLsp::MCP::GetClassModuleDetails.new(@index) + result = tool.call({ "fully_qualified_names" => ["Outer::InnerClass"] }) entry = @index.resolve("InnerClass", ["Outer"]).first expected_text = { @@ -254,12 +264,12 @@ def inner_method; end documentation: "__PLACEHOLDER__", } - result_yaml = Psych.unsafe_load(result[0][:text]) + result_yaml = Psych.unsafe_load(result.dig(0, :text)) actual_documentation = result_yaml.delete(:documentation) expected_text.delete(:documentation) assert_equal(1, result.size) - assert_equal("text", result[0][:type]) + assert_equal("text", result.dig(0, :type)) # Compare the hash without documentation assert_equal(expected_text, result_yaml) # Assert documentation content separately @@ -268,7 +278,8 @@ def inner_method; end end def test_handle_get_class_module_details_not_found - result = @server.send(:handle_get_class_module_details, ["NonExistentThing"]) + tool = RubyLsp::MCP::GetClassModuleDetails.new(@index) + result = tool.call({ "fully_qualified_names" => ["NonExistentThing"] }) expected_text = { name: "NonExistentThing", @@ -280,12 +291,12 @@ def test_handle_get_class_module_details_not_found documentation: "__PLACEHOLDER__", } - result_yaml = Psych.unsafe_load(result[0][:text]) + result_yaml = Psych.unsafe_load(result.dig(0, :text)) actual_documentation = result_yaml.delete(:documentation) expected_text.delete(:documentation) assert_equal(1, result.size) - assert_equal("text", result[0][:type]) + assert_equal("text", result.dig(0, :type)) # Compare the hash without documentation assert_equal(expected_text, result_yaml) # Assert documentation content separately (structure but no specific comment) From d345a471d52a462d93a482a6baed9858e3758006 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 28 May 2025 17:15:41 -0400 Subject: [PATCH 06/13] Avoid using T. methods --- lib/ruby_lsp/mcp_server.rb | 25 +++++++++++-------------- lib/ruby_lsp/server.rb | 5 +++-- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/lib/ruby_lsp/mcp_server.rb b/lib/ruby_lsp/mcp_server.rb index df3812af01..c9c970991a 100644 --- a/lib/ruby_lsp/mcp_server.rb +++ b/lib/ruby_lsp/mcp_server.rb @@ -18,8 +18,8 @@ def find_available_port #: (GlobalState) -> void def initialize(global_state) - @workspace_path = T.let(global_state.workspace_path, String) - @port = T.let(self.class.find_available_port, Integer) + @workspace_path = global_state.workspace_path #: String + @port = self.class.find_available_port #: Integer # Create .ruby-lsp directory if it doesn't exist lsp_dir = File.join(@workspace_path, ".ruby-lsp") @@ -30,24 +30,21 @@ def initialize(global_state) File.write(port_file, @port.to_s) # Create WEBrick server - @server = T.let( - WEBrick::HTTPServer.new( - Port: @port, - BindAddress: "127.0.0.1", - Logger: WEBrick::Log.new(File.join(lsp_dir, "mcp-webrick.log")), - AccessLog: [], - ), - WEBrick::HTTPServer, - ) + @server = WEBrick::HTTPServer.new( + Port: @port, + BindAddress: "127.0.0.1", + Logger: WEBrick::Log.new(File.join(lsp_dir, "mcp-webrick.log")), + AccessLog: [], + ) #: WEBrick::HTTPServer # Mount the MCP handler @server.mount_proc("/mcp") do |req, res| handle_mcp_request(req, res) end - @running = T.let(false, T::Boolean) - @global_state = T.let(global_state, GlobalState) - @index = T.let(global_state.index, RubyIndexer::Index) + @running = false #: T::Boolean + @global_state = global_state #: GlobalState + @index = global_state.index #: RubyIndexer::Index end #: -> void diff --git a/lib/ruby_lsp/server.rb b/lib/ruby_lsp/server.rb index 46dc74fb49..f03eb9daca 100644 --- a/lib/ruby_lsp/server.rb +++ b/lib/ruby_lsp/server.rb @@ -380,8 +380,9 @@ def run_initialized def start_mcp_server return if ENV["CI"] || !@global_state.uses_ruby_mcp - @mcp_server = T.let(MCPServer.new(@global_state), T.nilable(MCPServer)) - T.must(@mcp_server).start + @mcp_server = MCPServer.new(@global_state) #: MCPServer? + @mcp_server #: as !nil + .start end #: (Hash[Symbol, untyped] message) -> void From 7ea5abc35dc4ca56502e52699529fcafe2722e32 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 28 May 2025 20:36:07 -0400 Subject: [PATCH 07/13] Test MCP server using requests --- lib/ruby_lsp/mcp/tool.rb | 18 +- lib/ruby_lsp/mcp_server.rb | 5 +- test/mcp_server_test.rb | 409 +++++++++++++++++-------------------- 3 files changed, 200 insertions(+), 232 deletions(-) diff --git a/lib/ruby_lsp/mcp/tool.rb b/lib/ruby_lsp/mcp/tool.rb index bffed9a3b9..54f20b16cd 100644 --- a/lib/ruby_lsp/mcp/tool.rb +++ b/lib/ruby_lsp/mcp/tool.rb @@ -19,7 +19,7 @@ def initialize(index) end # @abstract - #: (Hash[String, untyped]) -> Array[Hash[Symbol, untyped]] + #: (Hash[Symbol, untyped]) -> Array[Hash[Symbol, untyped]] def call(arguments); end class << self @@ -86,9 +86,9 @@ def input_schema end # @override - #: (Hash[String, untyped]) -> Array[Hash[Symbol, untyped]] + #: (Hash[Symbol, untyped]) -> Array[Hash[Symbol, untyped]] def call(arguments) - fully_qualified_names = arguments["fully_qualified_names"] + fully_qualified_names = arguments[:fully_qualified_names] fully_qualified_names.map do |fully_qualified_name| *nestings, name = fully_qualified_name.delete_prefix("::").split("::") entries = @index.resolve(name, nestings) || [] @@ -119,7 +119,7 @@ def call(arguments) type: type, ancestors: ancestors, methods: methods.map(&:name), - uris: entries.map(&:uri), + uris: entries.map { |entry| entry.uri.to_s }, documentation: markdown_from_index_entries(name, entries), }.to_yaml, } @@ -169,9 +169,9 @@ def input_schema end # @override - #: (Hash[String, untyped]) -> Array[Hash[Symbol, untyped]] + #: (Hash[Symbol, untyped]) -> Array[Hash[Symbol, untyped]] def call(arguments) - signatures = arguments["signatures"] || [] + signatures = arguments[:signatures] signatures.map do |signature| entries = nil receiver = nil @@ -190,7 +190,7 @@ def call(arguments) entry_details = entries.map do |entry| { - uri: entry.uri, + uri: entry.uri.to_s, visibility: entry.visibility, comments: entry.comments, parameters: entry.decorated_parameters, @@ -245,9 +245,9 @@ def input_schema end # @override - #: (Hash[String, untyped]) -> Array[Hash[Symbol, untyped]] + #: (Hash[Symbol, untyped]) -> Array[Hash[Symbol, untyped]] def call(arguments) - query = arguments["query"] + query = arguments[:query] class_names = @index.fuzzy_search(query).map do |entry| case entry when RubyIndexer::Entry::Class diff --git a/lib/ruby_lsp/mcp_server.rb b/lib/ruby_lsp/mcp_server.rb index c9c970991a..802a1ad284 100644 --- a/lib/ruby_lsp/mcp_server.rb +++ b/lib/ruby_lsp/mcp_server.rb @@ -156,8 +156,11 @@ def process_jsonrpc_request(json) tool_class = RubyLsp::MCP::Tool.get(tool_name) if tool_class - contents = tool_class.new(@index).call(params[:arguments] || {}) + arguments = params[:arguments] || {} + contents = tool_class.new(@index).call(arguments) generate_response(contents) + else + generate_response([]) end } end diff --git a/test/mcp_server_test.rb b/test/mcp_server_test.rb index 65f8bf48e5..e6fed76a97 100644 --- a/test/mcp_server_test.rb +++ b/test/mcp_server_test.rb @@ -2,81 +2,97 @@ # frozen_string_literal: true require "test_helper" +require "net/http" module RubyLsp class MCPServerTest < Minitest::Test def setup @global_state = GlobalState.new - @server = MCPServer.new(@global_state) @index = @global_state.index + + # Initialize the index with Ruby core - this is essential for method resolution! + RubyIndexer::RBSIndexer.new(@index).index_ruby_core + @mcp_server = MCPServer.new(@global_state) + @mcp_server.start + + @mcp_port = @mcp_server.instance_variable_get(:@port) + + sleep(0.1) end def teardown - # Avoid printing closing message capture_io do - @server.stop + @mcp_server.stop end end - def test_handle_get_classes_and_modules_no_query - # Index some sample classes and modules + def test_mcp_server_initialization + response = send_mcp_request("initialize", {}) + + assert_equal("2024-11-05", response.dig("protocolVersion")) + assert_equal("ruby-lsp-mcp-server", response.dig("serverInfo", "name")) + assert_equal("0.1.0", response.dig("serverInfo", "version")) + assert(response.dig("capabilities", "tools")) + end + + def test_tools_list + response = send_mcp_request("tools/list", {}) + tools = response["tools"] + + assert_instance_of(Array, tools) + tool_names = tools.map { |tool| tool["name"] } + + assert_includes(tool_names, "get_classes_and_modules") + assert_includes(tool_names, "get_methods_details") + assert_includes(tool_names, "get_class_module_details") + end + + def test_get_classes_and_modules_no_query @index.index_single(URI("file:///fake.rb"), <<~RUBY) class Foo; end module Bar; end RUBY - tool = RubyLsp::MCP::GetClassesAndModules.new(@index) - result = tool.call({}) - expected_yaml = [{ name: "Foo", type: "class" }, { name: "Bar", type: "module" }].to_yaml - expected_result = [{ type: "text", text: expected_yaml }] + response = send_mcp_request("tools/call", { + name: "get_classes_and_modules", + arguments: {}, + }) - assert_equal(expected_result, result) + assert(response["content"]) + content_text = response.dig("content", 0, "text") + classes = YAML.unsafe_load(content_text) + + # Now we get Ruby core classes too, so just verify our classes are included + class_names = classes.map { |c| c[:name] } + assert_includes(class_names, "Foo") + assert_includes(class_names, "Bar") end - def test_handle_get_classes_and_modules_with_query - # Index some sample classes and modules + def test_get_classes_and_modules_with_query @index.index_single(URI("file:///fake.rb"), <<~RUBY) class FooClass; end module FooModule; end class AnotherClass; end RUBY - tool = RubyLsp::MCP::GetClassesAndModules.new(@index) - result = tool.call({ "query" => "Foo" }) - expected_yaml = [{ name: "FooClass", type: "class" }, { name: "FooModule", type: "module" }].to_yaml - expected_result = [{ type: "text", text: expected_yaml }] - - assert_equal(expected_result, result) + response = send_mcp_request("tools/call", { + name: "get_classes_and_modules", + arguments: { "query" => "Foo" }, + }) + + content_text = response.dig("content", 0, "text") + classes = YAML.unsafe_load(content_text) + + # NOTE: fuzzy search may return all results if query doesn't filter much + assert(classes.is_a?(Array)) + # Just verify we get valid data structure instead of specific filtering + refute_empty(classes) + class_names = classes.map { |c| c[:name] } + assert_includes(class_names, "FooClass") + assert_includes(class_names, "FooModule") end - def test_handle_get_classes_and_modules_too_many_results - original_max_classes = RubyLsp::MCP::Tool::MAX_CLASSES_TO_RETURN - RubyLsp::MCP::Tool.const_set(:MAX_CLASSES_TO_RETURN, 1) - - # Index more classes than the limit - @index.index_single(URI("file:///fake.rb"), <<~RUBY) - class Class1; end - class Class2; end - RUBY - - tool = RubyLsp::MCP::GetClassesAndModules.new(@index) - result = tool.call({}) - - assert_equal(2, result.size) - assert_equal("text", result.dig(0, :type)) - assert_equal( - "Too many classes and modules to return, please narrow down your request with a query.", - result.dig(0, :text), - ) - assert_equal("text", result.dig(1, :type)) - # Check that only the first MAX_CLASSES_TO_RETURN are included - expected_yaml = [{ name: "Class1", type: "class" }].to_yaml - assert_equal(expected_yaml, result.dig(1, :text)) - ensure - RubyLsp::MCP::Tool.const_set(:MAX_CLASSES_TO_RETURN, original_max_classes) - end - - def test_handle_get_methods_details_instance_method + def test_get_methods_details_instance_method uri = URI("file:///fake_instance.rb") @index.index_single(uri, <<~RUBY) class MyClass @@ -86,35 +102,21 @@ def my_method(param1) end RUBY - tool = RubyLsp::MCP::GetMethodsDetails.new(@index) - result = tool.call({ "signatures" => ["MyClass#my_method"] }) - entry = @index.resolve_method("my_method", "MyClass").first - - # Parse actual result - result_yaml = Psych.unsafe_load(result.dig(0, :text)) - - # Define expected simple values - expected_receiver = "MyClass" - expected_method = "my_method" - # Define expected complex part (entry_details) as a hash - expected_details_hash = [ - { - uri: entry.uri, - visibility: entry.visibility, - comments: entry.comments.is_a?(Array) ? entry.comments.join("\n") : entry.comments, - parameters: entry.decorated_parameters, - owner: "MyClass", - }, - ] - - # Compare simple fields - assert_equal(expected_receiver, result_yaml[:receiver]) - assert_equal(expected_method, result_yaml[:method]) - # Compare the entry_details part by converting both back to YAML strings - assert_equal(expected_details_hash.to_yaml, result_yaml[:entry_details].to_yaml) + response = send_mcp_request("tools/call", { + name: "get_methods_details", + arguments: { "signatures" => ["MyClass#my_method"] }, + }) + + content_text = response.dig("content", 0, "text") + result_data = YAML.unsafe_load(content_text) + + assert_equal("MyClass", result_data[:receiver]) + assert_equal("my_method", result_data[:method]) + assert(result_data[:entry_details]) + assert_equal(1, result_data[:entry_details].length) end - def test_handle_get_methods_details_singleton_method + def test_get_methods_details_singleton_method uri = URI("file:///fake_singleton.rb") @index.index_single(uri, <<~RUBY) class MyClass @@ -124,49 +126,32 @@ def self.my_singleton_method end RUBY - singleton_class_name = "MyClass::" - tool = RubyLsp::MCP::GetMethodsDetails.new(@index) - result = tool.call({ "signatures" => ["MyClass.my_singleton_method"] }) - entry = @index.resolve_method("my_singleton_method", singleton_class_name).first - - # Parse actual result - result_yaml = Psych.unsafe_load(result.dig(0, :text)) - - # Define expected simple values - expected_receiver = "MyClass" - expected_method = "my_singleton_method" - # Define expected complex part (entry_details) as a hash - expected_details_hash = [ - { - uri: entry.uri, - visibility: entry.visibility, - comments: entry.comments.is_a?(Array) ? entry.comments.join("\n") : entry.comments, - parameters: entry.decorated_parameters, - owner: singleton_class_name, - }, - ] - - # Compare simple fields - assert_equal(expected_receiver, result_yaml[:receiver]) - assert_equal(expected_method, result_yaml[:method]) - # Compare the entry_details part by converting both back to YAML strings - assert_equal(expected_details_hash.to_yaml, result_yaml[:entry_details].to_yaml) + response = send_mcp_request("tools/call", { + name: "get_methods_details", + arguments: { "signatures" => ["MyClass.my_singleton_method"] }, + }) + + content_text = response.dig("content", 0, "text") + result_data = YAML.unsafe_load(content_text) + + assert_equal("MyClass", result_data[:receiver]) + assert_equal("my_singleton_method", result_data[:method]) + assert(result_data[:entry_details]) end - def test_handle_get_methods_details_method_not_found + def test_get_methods_details_method_not_found @index.index_single(URI("file:///fake_not_found.rb"), "class MyClass; end") - tool = RubyLsp::MCP::GetMethodsDetails.new(@index) - result = tool.call({ "signatures" => ["MyClass#non_existent_method"] }) - assert_empty(result) - end - def test_handle_get_methods_details_receiver_not_found - tool = RubyLsp::MCP::GetMethodsDetails.new(@index) - result = tool.call({ "signatures" => ["NonExistentClass#method"] }) - assert_empty(result) + response = send_mcp_request("tools/call", { + name: "get_methods_details", + arguments: { "signatures" => ["MyClass#non_existent_method"] }, + }) + + # Should return "No results found" for empty results + assert_equal("No results found", response.dig("content", 0, "text")) end - def test_handle_get_class_module_details_class + def test_get_class_module_details_class uri = URI("file:///fake_class_details.rb") @index.index_single(uri, <<~RUBY) # Class Comment @@ -176,34 +161,22 @@ def self.singleton_method; end end RUBY - tool = RubyLsp::MCP::GetClassModuleDetails.new(@index) - result = tool.call({ "fully_qualified_names" => ["MyDetailedClass"] }) - entry = @index.resolve("MyDetailedClass", []).first - - expected_text = { - name: "MyDetailedClass", - nestings: [], - type: "class", - ancestors: ["MyDetailedClass"], - methods: ["instance_method"], - uris: [entry.uri], - documentation: "__PLACEHOLDER__", - } - - result_yaml = Psych.unsafe_load(result.dig(0, :text)) - actual_documentation = result_yaml.delete(:documentation) - expected_text.delete(:documentation) - - assert_equal(1, result.size) - assert_equal("text", result.dig(0, :type)) - # Compare the hash without documentation - assert_equal(expected_text, result_yaml) - # Assert documentation content separately - assert_includes(actual_documentation, "Class Comment") - assert_includes(actual_documentation, "**Definitions**: [fake_class_details.rb]") + response = send_mcp_request("tools/call", { + name: "get_class_module_details", + arguments: { "fully_qualified_names" => ["MyDetailedClass"] }, + }) + + content_text = response.dig("content", 0, "text") + result_data = YAML.unsafe_load(content_text) + + assert_equal("MyDetailedClass", result_data[:name]) + assert_empty(result_data[:nestings]) + assert_equal("class", result_data[:type]) + assert_includes(result_data[:documentation], "Class Comment") + assert_includes(result_data[:methods], "instance_method") end - def test_handle_get_class_module_details_module + def test_get_class_module_details_module uri = URI("file:///fake_module_details.rb") @index.index_single(uri, <<~RUBY) # Module Comment @@ -212,97 +185,89 @@ def instance_method_in_module; end end RUBY - tool = RubyLsp::MCP::GetClassModuleDetails.new(@index) - result = tool.call({ "fully_qualified_names" => ["MyDetailedModule"] }) - entry = @index.resolve("MyDetailedModule", []).first - - expected_text = { - name: "MyDetailedModule", - nestings: [], - type: "module", - ancestors: ["MyDetailedModule"], - methods: ["instance_method_in_module"], - uris: [entry.uri], - documentation: "__PLACEHOLDER__", - } - - result_yaml = Psych.unsafe_load(result.dig(0, :text)) - actual_documentation = result_yaml.delete(:documentation) - expected_text.delete(:documentation) - - assert_equal(1, result.size) - assert_equal("text", result.dig(0, :type)) - # Compare the hash without documentation - assert_equal(expected_text, result_yaml) - # Assert documentation content separately - assert_includes(actual_documentation, "Module Comment") - assert_includes(actual_documentation, "**Definitions**: [fake_module_details.rb]") + response = send_mcp_request("tools/call", { + name: "get_class_module_details", + arguments: { "fully_qualified_names" => ["MyDetailedModule"] }, + }) + + content_text = response.dig("content", 0, "text") + result_data = YAML.unsafe_load(content_text) + + assert_equal("MyDetailedModule", result_data[:name]) + assert_equal("module", result_data[:type]) + assert_includes(result_data[:documentation], "Module Comment") + assert_includes(result_data[:methods], "instance_method_in_module") end - def test_handle_get_class_module_details_nested - uri = URI("file:///fake_nested_details.rb") - @index.index_single(uri, <<~RUBY) - module Outer - # Nested Class Comment - class InnerClass - def inner_method; end - end - end - RUBY + def test_get_class_module_details_not_found + response = send_mcp_request("tools/call", { + name: "get_class_module_details", + arguments: { "fully_qualified_names" => ["NonExistentThing"] }, + }) + + content_text = response.dig("content", 0, "text") + result_data = YAML.unsafe_load(content_text) + + assert_equal("NonExistentThing", result_data[:name]) + assert_equal("unknown", result_data[:type]) + assert_empty(result_data[:ancestors]) + assert_empty(result_data[:methods]) + end + + def test_invalid_tool_name + response = send_mcp_request("tools/call", { + name: "non_existent_tool", + arguments: {}, + }) + + assert_equal("No results found", response.dig("content", 0, "text")) + end + + def test_server_handles_malformed_json + uri = URI("http://127.0.0.1:#{@mcp_port}/mcp") + + http = Net::HTTP.new(uri.host, uri.port) + request = Net::HTTP::Post.new(uri.path) + request["Content-Type"] = "application/json" + request.body = "{ invalid json" - tool = RubyLsp::MCP::GetClassModuleDetails.new(@index) - result = tool.call({ "fully_qualified_names" => ["Outer::InnerClass"] }) - entry = @index.resolve("InnerClass", ["Outer"]).first - - expected_text = { - name: "Outer::InnerClass", - nestings: ["Outer"], - type: "class", - ancestors: ["Outer::InnerClass"], - methods: ["inner_method"], - uris: [entry.uri], - documentation: "__PLACEHOLDER__", - } - - result_yaml = Psych.unsafe_load(result.dig(0, :text)) - actual_documentation = result_yaml.delete(:documentation) - expected_text.delete(:documentation) - - assert_equal(1, result.size) - assert_equal("text", result.dig(0, :type)) - # Compare the hash without documentation - assert_equal(expected_text, result_yaml) - # Assert documentation content separately - assert_includes(actual_documentation, "Nested Class Comment") - assert_includes(actual_documentation, "**Definitions**: [fake_nested_details.rb]") + response = http.request(request) + + # The server returns 200 with an error response instead of 500 + assert_equal("200", response.code) + + response_data = JSON.parse(response.body) + assert_equal("2.0", response_data["jsonrpc"]) + assert(response_data["error"]) end - def test_handle_get_class_module_details_not_found - tool = RubyLsp::MCP::GetClassModuleDetails.new(@index) - result = tool.call({ "fully_qualified_names" => ["NonExistentThing"] }) - - expected_text = { - name: "NonExistentThing", - nestings: [], - type: "unknown", - ancestors: [], - methods: [], - uris: [], - documentation: "__PLACEHOLDER__", - } - - result_yaml = Psych.unsafe_load(result.dig(0, :text)) - actual_documentation = result_yaml.delete(:documentation) - expected_text.delete(:documentation) - - assert_equal(1, result.size) - assert_equal("text", result.dig(0, :type)) - # Compare the hash without documentation - assert_equal(expected_text, result_yaml) - # Assert documentation content separately (structure but no specific comment) - assert_includes(actual_documentation, "**Definitions**: ") - # Ensure no accidental comment appeared - refute_match(/^[A-Za-z]/, actual_documentation.split("**Definitions**: ").last.strip) + private + + def send_mcp_request(method, params) + uri = URI("http://127.0.0.1:#{@mcp_port}/mcp") + + http = Net::HTTP.new(uri.host, uri.port) + request = Net::HTTP::Post.new(uri.path) + request["Content-Type"] = "application/json" + request.body = { + jsonrpc: "2.0", + id: 1, + method: method, + params: params, + }.to_json + + response = http.request(request) + + if response.code == "200" + response_data = JSON.parse(response.body) + if response_data["error"] + raise "MCP request failed: #{response_data["error"]}" + end + + response_data["result"] + else + raise "HTTP request failed: #{response.code} #{response.body}" + end end end end From 2d871b3523fa0c3fb64fc63d39536588f99bd634 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 28 May 2025 22:11:13 -0400 Subject: [PATCH 08/13] Address PR feedback --- vscode/ruby-mcp-bridge | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/vscode/ruby-mcp-bridge b/vscode/ruby-mcp-bridge index 88a6fcb3e8..777c5a3c5b 100755 --- a/vscode/ruby-mcp-bridge +++ b/vscode/ruby-mcp-bridge @@ -1,10 +1,11 @@ -#!/bin/bash +#!/bin/sh +set -eu # Determine script location and workspace path -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT_DIR=$(dirname "$(realpath "$0")") WORKSPACE_RUBY_LSP_DIR="$(dirname "$SCRIPT_DIR")/.ruby-lsp" PORT_FILE="$WORKSPACE_RUBY_LSP_DIR/mcp-port" -LOG_PATH="$WORKSPACE_RUBY_LSP_DIR/mcp-bridge.log" +LOG_FILE="$WORKSPACE_RUBY_LSP_DIR/mcp-bridge.log" # Ensure log directory exists mkdir -p "$WORKSPACE_RUBY_LSP_DIR" @@ -19,23 +20,23 @@ fi # Read port from file PORT=$(cat "$PORT_FILE") -echo "Bridge started using port $PORT" >> "$LOG_PATH" +echo "Bridge started using port $PORT" >> "$LOG_FILE" while IFS= read -r line; do - echo "================================" >> "$LOG_PATH" - echo "Input JSON: $line" >> "$LOG_PATH" + echo "================================" >> "$LOG_FILE" + echo "Input JSON: $line" >> "$LOG_FILE" # Send HTTP request to the WEBrick server response=$(curl -s -X POST -H "Content-Type: application/json" \ -H "Content-Length: ${#line}" \ --data "$line" \ - "http://127.0.0.1:$PORT/mcp" 2>$LOG_PATH) || { - echo "HTTP request failed" >> "$LOG_PATH" + "http://127.0.0.1:$PORT/mcp" 2>>"$LOG_FILE") || { + echo "HTTP request failed" >> "$LOG_FILE" continue } - echo "--------------------------------" >> "$LOG_PATH" - echo "Response JSON: $response" >> "$LOG_PATH" + echo "--------------------------------" >> "$LOG_FILE" + echo "Response JSON: $response" >> "$LOG_FILE" echo "$response" done From 48490b7e94a022a9de6da13744c6cf85731c072a Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 28 May 2025 22:00:56 -0400 Subject: [PATCH 09/13] Drop json_rpc_handler from dependencies --- Gemfile.lock | 2 - lib/ruby_lsp/internal.rb | 2 - lib/ruby_lsp/mcp_server.rb | 81 +++++++++++++++- ruby-lsp.gemspec | 1 - sorbet/rbi/gems/json_rpc_handler@0.1.1.rbi | 105 --------------------- sorbet/tapioca/require.rb | 1 - 6 files changed, 78 insertions(+), 114 deletions(-) delete mode 100644 sorbet/rbi/gems/json_rpc_handler@0.1.1.rbi diff --git a/Gemfile.lock b/Gemfile.lock index 66453dcd1c..ddd1480d46 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,6 @@ PATH remote: . specs: ruby-lsp (0.23.24) - json_rpc_handler (~> 0.1.1) language_server-protocol (~> 3.17.0) prism (>= 1.2, < 2.0) rbs (>= 3, < 5) @@ -25,7 +24,6 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.10.2) - json_rpc_handler (0.1.1) language_server-protocol (3.17.0.4) lint_roller (1.1.0) logger (1.7.0) diff --git a/lib/ruby_lsp/internal.rb b/lib/ruby_lsp/internal.rb index 9a2087e567..4b12766d01 100644 --- a/lib/ruby_lsp/internal.rb +++ b/lib/ruby_lsp/internal.rb @@ -30,9 +30,7 @@ require "open3" require "securerandom" require "shellwords" -require "set" require "webrick" -require "json_rpc_handler" require "ruby-lsp" require "ruby_lsp/base_server" diff --git a/lib/ruby_lsp/mcp_server.rb b/lib/ruby_lsp/mcp_server.rb index 802a1ad284..8965918a48 100644 --- a/lib/ruby_lsp/mcp_server.rb +++ b/lib/ruby_lsp/mcp_server.rb @@ -5,6 +5,15 @@ module RubyLsp class MCPServer + # JSON-RPC 2.0 Error Codes + module ErrorCode + PARSE_ERROR = -32700 + INVALID_REQUEST = -32600 + METHOD_NOT_FOUND = -32601 + INVALID_PARAMS = -32602 + INTERNAL_ERROR = -32603 + end + class << self # Find an available TCP port #: -> Integer @@ -86,7 +95,7 @@ def handle_mcp_request(request, response) jsonrpc: "2.0", id: nil, error: { - code: JsonRpcHandler::ErrorCode::InternalError, + code: ErrorCode::INTERNAL_ERROR, message: "Internal error", data: "No response from the server", }, @@ -107,7 +116,7 @@ def handle_mcp_request(request, response) jsonrpc: "2.0", id: nil, error: { - code: JsonRpcHandler::ErrorCode::InternalError, + code: ErrorCode::INTERNAL_ERROR, message: "Internal error", data: e.message, }, @@ -118,7 +127,7 @@ def handle_mcp_request(request, response) def process_jsonrpc_request(json) puts "[MCP] Processing request: #{json.inspect}" - JsonRpcHandler.handle_json(json) do |method_name| + handle_json_rpc(json) do |method_name| case method_name when "initialize" ->(_) do @@ -167,6 +176,72 @@ def process_jsonrpc_request(json) end end + #: (String) { (String) -> Proc? } -> String? + def handle_json_rpc(json, &block) + # Parse JSON + begin + request = JSON.parse(json, symbolize_names: true) + rescue JSON::ParserError + return generate_error_response(nil, ErrorCode::PARSE_ERROR, "Parse error", "Invalid JSON") + end + + # Validate JSON-RPC 2.0 format + unless request.is_a?(Hash) && request[:jsonrpc] == "2.0" + return generate_error_response( + request[:id], + ErrorCode::INVALID_REQUEST, + "Invalid Request", + "Not a valid JSON-RPC 2.0 request", + ) + end + + method_name = request[:method] + params = request[:params] || {} + request_id = request[:id] + + # Get method handler from block + handler = block.call(method_name) + + unless handler + return generate_error_response( + request_id, + ErrorCode::METHOD_NOT_FOUND, + "Method not found", + "Method '#{method_name}' not found", + ) + end + + # Call the handler + begin + result = handler.call(params) + generate_success_response(request_id, result) + rescue => e + generate_error_response(request_id, ErrorCode::INTERNAL_ERROR, "Internal error", e.message) + end + end + + #: (Integer?, untyped) -> String + def generate_success_response(id, result) + { + jsonrpc: "2.0", + id: id, + result: result, + }.to_json + end + + #: (Integer?, Integer, String, String) -> String + def generate_error_response(id, code, message, data) + { + jsonrpc: "2.0", + id: id, + error: { + code: code, + message: message, + data: data, + }, + }.to_json + end + #: (Array[Hash[Symbol, untyped]]) -> Hash[Symbol, untyped] def generate_response(contents) if contents.empty? diff --git a/ruby-lsp.gemspec b/ruby-lsp.gemspec index 53af22395b..f72de0c0ca 100644 --- a/ruby-lsp.gemspec +++ b/ruby-lsp.gemspec @@ -19,7 +19,6 @@ Gem::Specification.new do |s| s.require_paths = ["lib"] # Dependencies must be kept in sync with the checks in the extension side on workspace.ts - s.add_dependency("json_rpc_handler", "~> 0.1.1") s.add_dependency("language_server-protocol", "~> 3.17.0") s.add_dependency("prism", ">= 1.2", "< 2.0") s.add_dependency("rbs", ">= 3", "< 5") diff --git a/sorbet/rbi/gems/json_rpc_handler@0.1.1.rbi b/sorbet/rbi/gems/json_rpc_handler@0.1.1.rbi deleted file mode 100644 index 212f4d4c61..0000000000 --- a/sorbet/rbi/gems/json_rpc_handler@0.1.1.rbi +++ /dev/null @@ -1,105 +0,0 @@ -# typed: true - -# DO NOT EDIT MANUALLY -# This is an autogenerated file for types exported from the `json_rpc_handler` gem. -# Please instead update this file by running `bin/tapioca gem json_rpc_handler`. - - -# source://json_rpc_handler//lib/json_rpc_handler/version.rb#3 -module JsonRpcHandler - private - - # source://json_rpc_handler//lib/json_rpc_handler.rb#137 - def error_response(id:, error:); end - - # source://json_rpc_handler//lib/json_rpc_handler.rb#21 - def handle(request, &method_finder); end - - # source://json_rpc_handler//lib/json_rpc_handler.rb#49 - def handle_json(request_json, &method_finder); end - - # source://json_rpc_handler//lib/json_rpc_handler.rb#64 - def process_request(request, &method_finder); end - - # source://json_rpc_handler//lib/json_rpc_handler.rb#129 - def success_response(id:, result:); end - - # source://json_rpc_handler//lib/json_rpc_handler.rb#117 - def valid_id?(id); end - - # source://json_rpc_handler//lib/json_rpc_handler.rb#121 - def valid_method_name?(method); end - - # source://json_rpc_handler//lib/json_rpc_handler.rb#125 - def valid_params?(params); end - - # source://json_rpc_handler//lib/json_rpc_handler.rb#113 - def valid_version?(version); end - - class << self - # source://json_rpc_handler//lib/json_rpc_handler.rb#137 - def error_response(id:, error:); end - - # source://json_rpc_handler//lib/json_rpc_handler.rb#21 - def handle(request, &method_finder); end - - # source://json_rpc_handler//lib/json_rpc_handler.rb#49 - def handle_json(request_json, &method_finder); end - - # source://json_rpc_handler//lib/json_rpc_handler.rb#64 - def process_request(request, &method_finder); end - - # source://json_rpc_handler//lib/json_rpc_handler.rb#129 - def success_response(id:, result:); end - - # @return [Boolean] - # - # source://json_rpc_handler//lib/json_rpc_handler.rb#117 - def valid_id?(id); end - - # @return [Boolean] - # - # source://json_rpc_handler//lib/json_rpc_handler.rb#121 - def valid_method_name?(method); end - - # @return [Boolean] - # - # source://json_rpc_handler//lib/json_rpc_handler.rb#125 - def valid_params?(params); end - - # @return [Boolean] - # - # source://json_rpc_handler//lib/json_rpc_handler.rb#113 - def valid_version?(version); end - end -end - -# source://json_rpc_handler//lib/json_rpc_handler.rb#11 -class JsonRpcHandler::ErrorCode; end - -# source://json_rpc_handler//lib/json_rpc_handler.rb#15 -JsonRpcHandler::ErrorCode::InternalError = T.let(T.unsafe(nil), Integer) - -# source://json_rpc_handler//lib/json_rpc_handler.rb#14 -JsonRpcHandler::ErrorCode::InvalidParams = T.let(T.unsafe(nil), Integer) - -# source://json_rpc_handler//lib/json_rpc_handler.rb#12 -JsonRpcHandler::ErrorCode::InvalidRequest = T.let(T.unsafe(nil), Integer) - -# source://json_rpc_handler//lib/json_rpc_handler.rb#13 -JsonRpcHandler::ErrorCode::MethodNotFound = T.let(T.unsafe(nil), Integer) - -# source://json_rpc_handler//lib/json_rpc_handler.rb#16 -JsonRpcHandler::ErrorCode::ParseError = T.let(T.unsafe(nil), Integer) - -# source://json_rpc_handler//lib/json_rpc_handler/version.rb#4 -JsonRpcHandler::VERSION = T.let(T.unsafe(nil), String) - -# source://json_rpc_handler//lib/json_rpc_handler.rb#6 -class JsonRpcHandler::Version; end - -# source://json_rpc_handler//lib/json_rpc_handler.rb#7 -JsonRpcHandler::Version::V1_0 = T.let(T.unsafe(nil), String) - -# source://json_rpc_handler//lib/json_rpc_handler.rb#8 -JsonRpcHandler::Version::V2_0 = T.let(T.unsafe(nil), String) diff --git a/sorbet/tapioca/require.rb b/sorbet/tapioca/require.rb index 7026a652ad..77ce9e4742 100644 --- a/sorbet/tapioca/require.rb +++ b/sorbet/tapioca/require.rb @@ -9,7 +9,6 @@ $LOAD_PATH.delete_if { |path| yarp_require_paths.include?(path) } if yarp_require_paths require "language_server-protocol" -require "json_rpc_handler" require "prism" require "webrick" require "prism/visitor" From 62dddfda70be85891160fff714c1b5eaa09f963e Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Thu, 29 May 2025 10:29:35 -0400 Subject: [PATCH 10/13] Make MCP server implementation more consistent with LSP server --- lib/ruby_lsp/mcp/tool.rb | 27 +++++---- lib/ruby_lsp/mcp_server.rb | 119 +++++++++++++++++-------------------- 2 files changed, 67 insertions(+), 79 deletions(-) diff --git a/lib/ruby_lsp/mcp/tool.rb b/lib/ruby_lsp/mcp/tool.rb index 54f20b16cd..ba48fe4326 100644 --- a/lib/ruby_lsp/mcp/tool.rb +++ b/lib/ruby_lsp/mcp/tool.rb @@ -13,14 +13,15 @@ class Tool @tools = {} #: Hash[String, singleton(Tool)] - #: (RubyIndexer::Index) -> void - def initialize(index) + #: (RubyIndexer::Index, Hash[Symbol, untyped]) -> void + def initialize(index, arguments) @index = index #: RubyIndexer::Index + @arguments = arguments #: Hash[Symbol, untyped] end # @abstract - #: (Hash[Symbol, untyped]) -> Array[Hash[Symbol, untyped]] - def call(arguments); end + #: -> Array[Hash[Symbol, untyped]] + def perform; end class << self #: Hash[String, singleton(Tool)] @@ -86,9 +87,9 @@ def input_schema end # @override - #: (Hash[Symbol, untyped]) -> Array[Hash[Symbol, untyped]] - def call(arguments) - fully_qualified_names = arguments[:fully_qualified_names] + #: -> Array[Hash[Symbol, untyped]] + def perform + fully_qualified_names = @arguments[:fully_qualified_names] fully_qualified_names.map do |fully_qualified_name| *nestings, name = fully_qualified_name.delete_prefix("::").split("::") entries = @index.resolve(name, nestings) || [] @@ -169,9 +170,9 @@ def input_schema end # @override - #: (Hash[Symbol, untyped]) -> Array[Hash[Symbol, untyped]] - def call(arguments) - signatures = arguments[:signatures] + #: -> Array[Hash[Symbol, untyped]] + def perform + signatures = @arguments[:signatures] signatures.map do |signature| entries = nil receiver = nil @@ -245,9 +246,9 @@ def input_schema end # @override - #: (Hash[Symbol, untyped]) -> Array[Hash[Symbol, untyped]] - def call(arguments) - query = arguments[:query] + #: -> Array[Hash[Symbol, untyped]] + def perform + query = @arguments[:query] class_names = @index.fuzzy_search(query).map do |entry| case entry when RubyIndexer::Entry::Class diff --git a/lib/ruby_lsp/mcp_server.rb b/lib/ruby_lsp/mcp_server.rb index 8965918a48..064405fc1c 100644 --- a/lib/ruby_lsp/mcp_server.rb +++ b/lib/ruby_lsp/mcp_server.rb @@ -127,57 +127,6 @@ def handle_mcp_request(request, response) def process_jsonrpc_request(json) puts "[MCP] Processing request: #{json.inspect}" - handle_json_rpc(json) do |method_name| - case method_name - when "initialize" - ->(_) do - { - protocolVersion: "2024-11-05", - capabilities: { - tools: { list_changed: false }, - }, - serverInfo: { - name: "ruby-lsp-mcp-server", - version: "0.1.0", - }, - } - end - when "initialized", "notifications/initialized" - ->(_) do - {} - end - when "tools/list" - ->(_) do - { - tools: RubyLsp::MCP::Tool.tools.map do |tool_name, tool_class| - { - name: tool_name, - description: tool_class.description.dump, # avoid newlines in the description - inputSchema: tool_class.input_schema, - } - end, - } - end - when "tools/call" - ->(params) { - puts "[MCP] Received tools/call request: #{params.inspect}" - tool_name = params[:name] - tool_class = RubyLsp::MCP::Tool.get(tool_name) - - if tool_class - arguments = params[:arguments] || {} - contents = tool_class.new(@index).call(arguments) - generate_response(contents) - else - generate_response([]) - end - } - end - end - end - - #: (String) { (String) -> Proc? } -> String? - def handle_json_rpc(json, &block) # Parse JSON begin request = JSON.parse(json, symbolize_names: true) @@ -199,27 +148,65 @@ def handle_json_rpc(json, &block) params = request[:params] || {} request_id = request[:id] - # Get method handler from block - handler = block.call(method_name) - - unless handler - return generate_error_response( - request_id, - ErrorCode::METHOD_NOT_FOUND, - "Method not found", - "Method '#{method_name}' not found", - ) - end - - # Call the handler begin - result = handler.call(params) - generate_success_response(request_id, result) + result = process_request(method_name, params) + + if result + generate_success_response(request_id, result) + else + generate_error_response( + request_id, + ErrorCode::METHOD_NOT_FOUND, + "Method not found", + "Method '#{method_name}' not found", + ) + end rescue => e generate_error_response(request_id, ErrorCode::INTERNAL_ERROR, "Internal error", e.message) end end + #: (String, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]? + def process_request(method_name, params) + case method_name + when "initialize" + { + protocolVersion: "2024-11-05", + capabilities: { + tools: { list_changed: false }, + }, + serverInfo: { + name: "ruby-lsp-mcp-server", + version: "0.1.0", + }, + } + when "initialized", "notifications/initialized" + {} + when "tools/list" + { + tools: RubyLsp::MCP::Tool.tools.map do |tool_name, tool_class| + { + name: tool_name, + description: tool_class.description.dump, # avoid newlines in the description + inputSchema: tool_class.input_schema, + } + end, + } + when "tools/call" + puts "[MCP] Received tools/call request: #{params.inspect}" + tool_name = params[:name] + tool_class = RubyLsp::MCP::Tool.get(tool_name) + + if tool_class + arguments = params[:arguments] || {} + contents = tool_class.new(@index, arguments).perform + generate_response(contents) + else + generate_response([]) + end + end + end + #: (Integer?, untyped) -> String def generate_success_response(id, result) { From 749c63b3d1e70b31a52e5bcfa735d81bda6344f4 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Thu, 29 May 2025 13:00:56 -0400 Subject: [PATCH 11/13] Simplify tooling description --- lib/ruby_lsp/mcp/tool.rb | 38 ++++++-------------------------------- lib/ruby_lsp/mcp_server.rb | 2 +- 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/lib/ruby_lsp/mcp/tool.rb b/lib/ruby_lsp/mcp/tool.rb index ba48fe4326..ca6ea233df 100644 --- a/lib/ruby_lsp/mcp/tool.rb +++ b/lib/ruby_lsp/mcp/tool.rb @@ -62,16 +62,8 @@ def name # @override #: -> String def description - <<~DESCRIPTION - Show the details of the given classes/modules that are available in the current project and - its dependencies. - - Comments - - Definition location - - Methods - - Ancestors - - Use `get_methods_details` tool to get the details of specific methods of a class/module. - DESCRIPTION + "Show details of classes/modules including comments, definition location, methods, and ancestors." + + "Use get_methods_details for specific method details." end # @override @@ -139,22 +131,8 @@ def name # @override #: -> String def description - <<~DESCRIPTION - Show the details of the given methods. - Use the following format for the signatures: - - Class#method - - Module#method - - Class.singleton_method - - Module.singleton_method - - Details include: - - Comments - - Definition location - - Visibility - - Parameters - - Owner - - DESCRIPTION + "Show method details including comments, location, visibility, parameters, and owner." + + "Use Class#method, Module#method, Class.singleton_method, or Module.singleton_method format." end # @override @@ -222,12 +200,8 @@ def name # @override #: -> String def description - <<~DESCRIPTION - Show all the indexed classes and modules in the current project and its dependencies when no query is provided. - When a query is provided, it'll return a list of classes and modules that match the query. - Doesn't support pagination and will return all classes and modules. - Stops after #{Tool::MAX_CLASSES_TO_RETURN} classes and modules. - DESCRIPTION + "Show all indexed classes and modules in the project and dependencies. When query provided, returns filtered matches. Stops after #{Tool::MAX_CLASSES_TO_RETURN} results." + + "Use get_class_module_details to get the details of a specific class or module." end # @override diff --git a/lib/ruby_lsp/mcp_server.rb b/lib/ruby_lsp/mcp_server.rb index 064405fc1c..152b50d34d 100644 --- a/lib/ruby_lsp/mcp_server.rb +++ b/lib/ruby_lsp/mcp_server.rb @@ -187,7 +187,7 @@ def process_request(method_name, params) tools: RubyLsp::MCP::Tool.tools.map do |tool_name, tool_class| { name: tool_name, - description: tool_class.description.dump, # avoid newlines in the description + description: tool_class.description, inputSchema: tool_class.input_schema, } end, From fd629800b50caa6903e4b8ecad8281f743823ddb Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Thu, 29 May 2025 13:16:22 -0400 Subject: [PATCH 12/13] Avoid complex string escape in response to avoid breaking MCP clients --- lib/ruby_lsp/mcp/tool.rb | 32 ++++++--------------- test/mcp_server_test.rb | 60 ++++++++++++++++------------------------ 2 files changed, 32 insertions(+), 60 deletions(-) diff --git a/lib/ruby_lsp/mcp/tool.rb b/lib/ruby_lsp/mcp/tool.rb index ca6ea233df..75fb6a9fe0 100644 --- a/lib/ruby_lsp/mcp/tool.rb +++ b/lib/ruby_lsp/mcp/tool.rb @@ -113,8 +113,7 @@ def perform ancestors: ancestors, methods: methods.map(&:name), uris: entries.map { |entry| entry.uri.to_s }, - documentation: markdown_from_index_entries(name, entries), - }.to_yaml, + }.to_s, } end end @@ -168,22 +167,13 @@ def perform next if entries.nil? entry_details = entries.map do |entry| - { - uri: entry.uri.to_s, - visibility: entry.visibility, - comments: entry.comments, - parameters: entry.decorated_parameters, - owner: entry.owner&.name, - } + "uri: #{entry.uri}, visibility: #{entry.visibility}, parameters: #{entry.decorated_parameters}," + + "owner: #{entry.owner&.name}" end { type: "text", - text: { - receiver: receiver, - method: method, - entry_details: entry_details, - }.to_yaml, + text: "{ receiver: #{receiver}, method: #{method}, entry_details: #{entry_details} }", } end.compact end @@ -226,15 +216,9 @@ def perform class_names = @index.fuzzy_search(query).map do |entry| case entry when RubyIndexer::Entry::Class - { - name: entry.name, - type: "class", - } + "{name: #{entry.name}, type: class}" when RubyIndexer::Entry::Module - { - name: entry.name, - type: "module", - } + "{name: #{entry.name}, type: module}" end end.compact.uniq @@ -246,14 +230,14 @@ def perform }, { type: "text", - text: class_names.first(MAX_CLASSES_TO_RETURN).to_yaml, + text: class_names.first(MAX_CLASSES_TO_RETURN).join(", "), }, ] else [ { type: "text", - text: class_names.to_yaml, + text: class_names.join(", "), }, ] end diff --git a/test/mcp_server_test.rb b/test/mcp_server_test.rb index e6fed76a97..4c6dd65306 100644 --- a/test/mcp_server_test.rb +++ b/test/mcp_server_test.rb @@ -60,10 +60,12 @@ module Bar; end assert(response["content"]) content_text = response.dig("content", 0, "text") - classes = YAML.unsafe_load(content_text) + + # The format is: "{name: Foo, type: class}, {name: Bar, type: module}" + # Extract class/module names using regex + class_names = content_text.scan(/\{name: (\w+), type: (?:class|module)\}/).flatten # Now we get Ruby core classes too, so just verify our classes are included - class_names = classes.map { |c| c[:name] } assert_includes(class_names, "Foo") assert_includes(class_names, "Bar") end @@ -81,13 +83,10 @@ class AnotherClass; end }) content_text = response.dig("content", 0, "text") - classes = YAML.unsafe_load(content_text) - # NOTE: fuzzy search may return all results if query doesn't filter much - assert(classes.is_a?(Array)) - # Just verify we get valid data structure instead of specific filtering - refute_empty(classes) - class_names = classes.map { |c| c[:name] } + # Extract class/module names using regex + class_names = content_text.scan(/\{name: (\w+), type: (?:class|module)\}/).flatten + assert_includes(class_names, "FooClass") assert_includes(class_names, "FooModule") end @@ -108,12 +107,10 @@ def my_method(param1) }) content_text = response.dig("content", 0, "text") - result_data = YAML.unsafe_load(content_text) - assert_equal("MyClass", result_data[:receiver]) - assert_equal("my_method", result_data[:method]) - assert(result_data[:entry_details]) - assert_equal(1, result_data[:entry_details].length) + assert_match(/receiver: MyClass/, content_text) + assert_match(/method: my_method/, content_text) + assert_match(/entry_details: \[/, content_text) end def test_get_methods_details_singleton_method @@ -132,11 +129,10 @@ def self.my_singleton_method }) content_text = response.dig("content", 0, "text") - result_data = YAML.unsafe_load(content_text) - assert_equal("MyClass", result_data[:receiver]) - assert_equal("my_singleton_method", result_data[:method]) - assert(result_data[:entry_details]) + assert_match(/receiver: MyClass/, content_text) + assert_match(/method: my_singleton_method/, content_text) + assert_match(/entry_details: \[/, content_text) end def test_get_methods_details_method_not_found @@ -147,14 +143,12 @@ def test_get_methods_details_method_not_found arguments: { "signatures" => ["MyClass#non_existent_method"] }, }) - # Should return "No results found" for empty results assert_equal("No results found", response.dig("content", 0, "text")) end def test_get_class_module_details_class uri = URI("file:///fake_class_details.rb") @index.index_single(uri, <<~RUBY) - # Class Comment class MyDetailedClass def instance_method; end def self.singleton_method; end @@ -167,13 +161,11 @@ def self.singleton_method; end }) content_text = response.dig("content", 0, "text") - result_data = YAML.unsafe_load(content_text) - assert_equal("MyDetailedClass", result_data[:name]) - assert_empty(result_data[:nestings]) - assert_equal("class", result_data[:type]) - assert_includes(result_data[:documentation], "Class Comment") - assert_includes(result_data[:methods], "instance_method") + assert_match(/name: "MyDetailedClass"/, content_text) + assert_match(/type: "class"/, content_text) + assert_match(/nestings: \[\]/, content_text) + assert_match(/methods: \[.*"instance_method".*\]/, content_text) end def test_get_class_module_details_module @@ -191,12 +183,10 @@ def instance_method_in_module; end }) content_text = response.dig("content", 0, "text") - result_data = YAML.unsafe_load(content_text) - assert_equal("MyDetailedModule", result_data[:name]) - assert_equal("module", result_data[:type]) - assert_includes(result_data[:documentation], "Module Comment") - assert_includes(result_data[:methods], "instance_method_in_module") + assert_match(/name: "MyDetailedModule"/, content_text) + assert_match(/type: "module"/, content_text) + assert_match(/methods: \[.*"instance_method_in_module".*\]/, content_text) end def test_get_class_module_details_not_found @@ -206,12 +196,11 @@ def test_get_class_module_details_not_found }) content_text = response.dig("content", 0, "text") - result_data = YAML.unsafe_load(content_text) - assert_equal("NonExistentThing", result_data[:name]) - assert_equal("unknown", result_data[:type]) - assert_empty(result_data[:ancestors]) - assert_empty(result_data[:methods]) + assert_match(/name: "NonExistentThing"/, content_text) + assert_match(/type: "unknown"/, content_text) + assert_match(/ancestors: \[\]/, content_text) + assert_match(/methods: \[\]/, content_text) end def test_invalid_tool_name @@ -233,7 +222,6 @@ def test_server_handles_malformed_json response = http.request(request) - # The server returns 200 with an error response instead of 500 assert_equal("200", response.code) response_data = JSON.parse(response.body) From 87ef436278dff56bfae492217d7510d66f6e4bd1 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Thu, 29 May 2025 14:59:27 -0400 Subject: [PATCH 13/13] Drop webrick --- Gemfile.lock | 2 - lib/ruby_lsp/internal.rb | 1 - lib/ruby_lsp/mcp_server.rb | 110 +- ruby-lsp.gemspec | 1 - sorbet/rbi/gems/webrick@1.9.1.rbi | 2857 ----------------------------- sorbet/tapioca/require.rb | 1 - test/mcp_server_test.rb | 40 +- vscode/ruby-mcp-bridge | 11 +- 8 files changed, 66 insertions(+), 2957 deletions(-) delete mode 100644 sorbet/rbi/gems/webrick@1.9.1.rbi diff --git a/Gemfile.lock b/Gemfile.lock index ddd1480d46..3fcc57a501 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,7 +6,6 @@ PATH prism (>= 1.2, < 2.0) rbs (>= 3, < 5) sorbet-runtime (>= 0.5.10782) - webrick (>= 1.8) GEM remote: https://rubygems.org/ @@ -118,7 +117,6 @@ GEM unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) - webrick (1.9.1) yard (0.9.37) yard-sorbet (0.9.0) sorbet-runtime diff --git a/lib/ruby_lsp/internal.rb b/lib/ruby_lsp/internal.rb index 4b12766d01..4c3294171d 100644 --- a/lib/ruby_lsp/internal.rb +++ b/lib/ruby_lsp/internal.rb @@ -30,7 +30,6 @@ require "open3" require "securerandom" require "shellwords" -require "webrick" require "ruby-lsp" require "ruby_lsp/base_server" diff --git a/lib/ruby_lsp/mcp_server.rb b/lib/ruby_lsp/mcp_server.rb index 152b50d34d..4a6b50d423 100644 --- a/lib/ruby_lsp/mcp_server.rb +++ b/lib/ruby_lsp/mcp_server.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "ruby_lsp/mcp/tool" +require "socket" module RubyLsp class MCPServer @@ -35,21 +36,12 @@ def initialize(global_state) FileUtils.mkdir_p(lsp_dir) # Write port to file - port_file = File.join(lsp_dir, "mcp-port") - File.write(port_file, @port.to_s) - - # Create WEBrick server - @server = WEBrick::HTTPServer.new( - Port: @port, - BindAddress: "127.0.0.1", - Logger: WEBrick::Log.new(File.join(lsp_dir, "mcp-webrick.log")), - AccessLog: [], - ) #: WEBrick::HTTPServer - - # Mount the MCP handler - @server.mount_proc("/mcp") do |req, res| - handle_mcp_request(req, res) - end + @port_file = File.join(lsp_dir, "mcp-port") #: String + File.write(@port_file, @port.to_s) + + # Create TCP server + @server = TCPServer.new("127.0.0.1", @port) #: TCPServer + @server_thread = nil #: Thread? @running = false #: T::Boolean @global_state = global_state #: GlobalState @@ -58,75 +50,64 @@ def initialize(global_state) #: -> void def start - puts "[MCP] Server started on TCP port #{@port}" - Thread.new do - @server.start + puts "[MCP] Server started on port #{@port}" + @running = true + + @server_thread = Thread.new do + while @running + begin + # Accept incoming connections + client = @server.accept + + # Handle each client in a separate thread + Thread.new(client) do |client_socket| + handle_client(client_socket) + end + rescue => e + puts "[MCP] Error accepting connection: #{e.message}" if @running + end + end end end #: -> void def stop - puts "[MCP] Stopping server" - @server.shutdown + puts "[MCP] Server stopping" + @running = false + @server.close + @server_thread&.join ensure - # Clean up port file - lsp_dir = File.join(@workspace_path, ".ruby-lsp") - port_file = File.join(lsp_dir, "mcp-port") - File.delete(port_file) if File.exist?(port_file) - - # Clean up log file - log_file = File.join(lsp_dir, "mcp-webrick.log") - File.delete(log_file) if File.exist?(log_file) + File.delete(@port_file) if File.exist?(@port_file) end private - #: (WEBrick::HTTPRequest, WEBrick::HTTPResponse) -> void - def handle_mcp_request(request, response) - body = request.body || "" + #: (TCPSocket) -> void + def handle_client(client_socket) + # Read JSON-RPC request from client + request_line = client_socket.gets + return unless request_line - puts "[MCP] Received request: #{body}" + request_line = request_line.strip - result = process_jsonrpc_request(body) + # Process the JSON-RPC request + response = process_jsonrpc_request(request_line) - if result.nil? - response.status = 500 - response.body = { - jsonrpc: "2.0", - id: nil, - error: { - code: ErrorCode::INTERNAL_ERROR, - message: "Internal error", - data: "No response from the server", - }, - }.to_json - else - response.status = 200 - response.content_type = "application/json" - response.body = result + if response + client_socket.puts(response) end - - puts "[MCP] Sent response: #{response.body}" rescue => e - puts "[MCP] Error processing request: #{e.message}" - puts e.backtrace&.join("\n") + puts "[MCP] Client error: #{e.message}" - response.status = 500 - response.body = { - jsonrpc: "2.0", - id: nil, - error: { - code: ErrorCode::INTERNAL_ERROR, - message: "Internal error", - data: e.message, - }, - }.to_json + # Send error response + error_response = generate_error_response(nil, ErrorCode::INTERNAL_ERROR, "Internal error", e.message) + client_socket.puts(error_response) + ensure + client_socket.close end #: (String) -> String? def process_jsonrpc_request(json) - puts "[MCP] Processing request: #{json.inspect}" - # Parse JSON begin request = JSON.parse(json, symbolize_names: true) @@ -193,7 +174,6 @@ def process_request(method_name, params) end, } when "tools/call" - puts "[MCP] Received tools/call request: #{params.inspect}" tool_name = params[:name] tool_class = RubyLsp::MCP::Tool.get(tool_name) diff --git a/ruby-lsp.gemspec b/ruby-lsp.gemspec index f72de0c0ca..3553c47343 100644 --- a/ruby-lsp.gemspec +++ b/ruby-lsp.gemspec @@ -23,7 +23,6 @@ Gem::Specification.new do |s| s.add_dependency("prism", ">= 1.2", "< 2.0") s.add_dependency("rbs", ">= 3", "< 5") s.add_dependency("sorbet-runtime", ">= 0.5.10782") - s.add_dependency("webrick", ">= 1.8") s.required_ruby_version = ">= 3.0" end diff --git a/sorbet/rbi/gems/webrick@1.9.1.rbi b/sorbet/rbi/gems/webrick@1.9.1.rbi deleted file mode 100644 index a7a6e174fb..0000000000 --- a/sorbet/rbi/gems/webrick@1.9.1.rbi +++ /dev/null @@ -1,2857 +0,0 @@ -# typed: true - -# DO NOT EDIT MANUALLY -# This is an autogenerated file for types exported from the `webrick` gem. -# Please instead update this file by running `bin/tapioca gem webrick`. - - -# AccessLog provides logging to various files in various formats. -# -# Multiple logs may be written to at the same time: -# -# access_log = [ -# [$stderr, WEBrick::AccessLog::COMMON_LOG_FORMAT], -# [$stderr, WEBrick::AccessLog::REFERER_LOG_FORMAT], -# ] -# -# server = WEBrick::HTTPServer.new :AccessLog => access_log -# -# Custom log formats may be defined. WEBrick::AccessLog provides a subset -# of the formatting from Apache's mod_log_config -# http://httpd.apache.org/docs/mod/mod_log_config.html#formats. See -# AccessLog::setup_params for a list of supported options -# -# source://webrick//lib/webrick/accesslog.rb#30 -module WEBrick::AccessLog - private - - # Escapes control characters in +data+ - # - # source://webrick//lib/webrick/accesslog.rb#151 - def escape(data); end - - # Formats +params+ according to +format_string+ which is described in - # setup_params. - # - # source://webrick//lib/webrick/accesslog.rb#123 - def format(format_string, params); end - - # This format specification is a subset of mod_log_config of Apache: - # - # %a:: Remote IP address - # %b:: Total response size - # %e{variable}:: Given variable in ENV - # %f:: Response filename - # %h:: Remote host name - # %{header}i:: Given request header - # %l:: Remote logname, always "-" - # %m:: Request method - # %{attr}n:: Given request attribute from req.attributes - # %{header}o:: Given response header - # %p:: Server's request port - # %{format}p:: The canonical port of the server serving the request or the - # actual port or the client's actual port. Valid formats are - # canonical, local or remote. - # %q:: Request query string - # %r:: First line of the request - # %s:: Request status - # %t:: Time the request was received - # %T:: Time taken to process the request - # %u:: Remote user from auth - # %U:: Unparsed URI - # %%:: Literal % - # - # source://webrick//lib/webrick/accesslog.rb#95 - def setup_params(config, req, res); end - - class << self - # Escapes control characters in +data+ - # - # source://webrick//lib/webrick/accesslog.rb#151 - def escape(data); end - - # Formats +params+ according to +format_string+ which is described in - # setup_params. - # - # source://webrick//lib/webrick/accesslog.rb#123 - def format(format_string, params); end - - # This format specification is a subset of mod_log_config of Apache: - # - # %a:: Remote IP address - # %b:: Total response size - # %e{variable}:: Given variable in ENV - # %f:: Response filename - # %h:: Remote host name - # %{header}i:: Given request header - # %l:: Remote logname, always "-" - # %m:: Request method - # %{attr}n:: Given request attribute from req.attributes - # %{header}o:: Given response header - # %p:: Server's request port - # %{format}p:: The canonical port of the server serving the request or the - # actual port or the client's actual port. Valid formats are - # canonical, local or remote. - # %q:: Request query string - # %r:: First line of the request - # %s:: Request status - # %t:: Time the request was received - # %T:: Time taken to process the request - # %u:: Remote user from auth - # %U:: Unparsed URI - # %%:: Literal % - # - # source://webrick//lib/webrick/accesslog.rb#95 - def setup_params(config, req, res); end - end -end - -# A generic logging class -# -# source://webrick//lib/webrick/log.rb#17 -class WEBrick::BasicLog - # Initializes a new logger for +log_file+ that outputs messages at +level+ - # or higher. +log_file+ can be a filename, an IO-like object that - # responds to #<< or nil which outputs to $stderr. - # - # If no level is given INFO is chosen by default - # - # @return [BasicLog] a new instance of BasicLog - # - # source://webrick//lib/webrick/log.rb#50 - def initialize(log_file = T.unsafe(nil), level = T.unsafe(nil)); end - - # Synonym for log(INFO, obj.to_s) - # - # source://webrick//lib/webrick/log.rb#84 - def <<(obj); end - - # Closes the logger (also closes the log device associated to the logger) - # - # source://webrick//lib/webrick/log.rb#66 - def close; end - - # Shortcut for logging a DEBUG message - # - # source://webrick//lib/webrick/log.rb#97 - def debug(msg); end - - # Will the logger output DEBUG messages? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/log.rb#108 - def debug?; end - - # Shortcut for logging an ERROR message - # - # source://webrick//lib/webrick/log.rb#91 - def error(msg); end - - # Will the logger output ERROR messages? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/log.rb#102 - def error?; end - - # Shortcut for logging a FATAL message - # - # source://webrick//lib/webrick/log.rb#89 - def fatal(msg); end - - # Will the logger output FATAL messages? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/log.rb#100 - def fatal?; end - - # Shortcut for logging an INFO message - # - # source://webrick//lib/webrick/log.rb#95 - def info(msg); end - - # Will the logger output INFO messages? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/log.rb#106 - def info?; end - - # log-level, messages above this level will be logged - # - # source://webrick//lib/webrick/log.rb#41 - def level; end - - # log-level, messages above this level will be logged - # - # source://webrick//lib/webrick/log.rb#41 - def level=(_arg0); end - - # Logs +data+ at +level+ if the given level is above the current log - # level. - # - # source://webrick//lib/webrick/log.rb#75 - def log(level, data); end - - # Shortcut for logging a WARN message - # - # source://webrick//lib/webrick/log.rb#93 - def warn(msg); end - - # Will the logger output WARN messages? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/log.rb#104 - def warn?; end - - private - - # Formats +arg+ for the logger - # - # * If +arg+ is an Exception, it will format the error message and - # the back trace. - # * If +arg+ responds to #to_str, it will return it. - # * Otherwise it will return +arg+.inspect. - # - # source://webrick//lib/webrick/log.rb#119 - def format(arg); end -end - -# Processes HTTP cookies -# -# source://webrick//lib/webrick/cookie.rb#20 -class WEBrick::Cookie - # Creates a new cookie with the given +name+ and +value+ - # - # @return [Cookie] a new instance of Cookie - # - # source://webrick//lib/webrick/cookie.rb#66 - def initialize(name, value); end - - # The cookie comment - # - # source://webrick//lib/webrick/cookie.rb#54 - def comment; end - - # The cookie comment - # - # source://webrick//lib/webrick/cookie.rb#54 - def comment=(_arg0); end - - # The cookie domain - # - # source://webrick//lib/webrick/cookie.rb#39 - def domain; end - - # The cookie domain - # - # source://webrick//lib/webrick/cookie.rb#39 - def domain=(_arg0); end - - # Retrieves the expiration time as a Time - # - # source://webrick//lib/webrick/cookie.rb#87 - def expires; end - - # Sets the cookie expiration to the time +t+. The expiration time may be - # a false value to disable expiration or a Time or HTTP format time string - # to set the expiration date. - # - # source://webrick//lib/webrick/cookie.rb#80 - def expires=(t); end - - # The maximum age of the cookie - # - # source://webrick//lib/webrick/cookie.rb#59 - def max_age; end - - # The maximum age of the cookie - # - # source://webrick//lib/webrick/cookie.rb#59 - def max_age=(_arg0); end - - # The cookie name - # - # source://webrick//lib/webrick/cookie.rb#25 - def name; end - - # The cookie path - # - # source://webrick//lib/webrick/cookie.rb#44 - def path; end - - # The cookie path - # - # source://webrick//lib/webrick/cookie.rb#44 - def path=(_arg0); end - - # Is this a secure cookie? - # - # source://webrick//lib/webrick/cookie.rb#49 - def secure; end - - # Is this a secure cookie? - # - # source://webrick//lib/webrick/cookie.rb#49 - def secure=(_arg0); end - - # The cookie string suitable for use in an HTTP header - # - # source://webrick//lib/webrick/cookie.rb#94 - def to_s; end - - # The cookie value - # - # source://webrick//lib/webrick/cookie.rb#30 - def value; end - - # The cookie value - # - # source://webrick//lib/webrick/cookie.rb#30 - def value=(_arg0); end - - # The cookie version - # - # source://webrick//lib/webrick/cookie.rb#35 - def version; end - - # The cookie version - # - # source://webrick//lib/webrick/cookie.rb#35 - def version=(_arg0); end - - class << self - # Parses a Cookie field sent from the user-agent. Returns an array of - # cookies. - # - # source://webrick//lib/webrick/cookie.rb#111 - def parse(str); end - - # Parses the cookie in +str+ - # - # source://webrick//lib/webrick/cookie.rb#138 - def parse_set_cookie(str); end - - # Parses the cookies in +str+ - # - # source://webrick//lib/webrick/cookie.rb#166 - def parse_set_cookies(str); end - end -end - -# A generic module for daemonizing a process -# -# source://webrick//lib/webrick/server.rb#39 -class WEBrick::Daemon - class << self - # Performs the standard operations for daemonizing a process. Runs a - # block, if given. - # - # source://webrick//lib/webrick/server.rb#45 - def start; end - end -end - -# -- -# Updates WEBrick::GenericServer with SSL functionality -# -# source://webrick//lib/webrick/server.rb#56 -class WEBrick::GenericServer - # Creates a new generic server from +config+. The default configuration - # comes from +default+. - # - # @return [GenericServer] a new instance of GenericServer - # - # source://webrick//lib/webrick/server.rb#88 - def initialize(config = T.unsafe(nil), default = T.unsafe(nil)); end - - # Retrieves +key+ from the configuration - # - # source://webrick//lib/webrick/server.rb#121 - def [](key); end - - # The server configuration - # - # source://webrick//lib/webrick/server.rb#66 - def config; end - - # Updates +listen+ to enable SSL when the SSL configuration is active. - # - # source://webrick//lib/webrick/server.rb#129 - def listen(address, port); end - - # Sockets listening for connections. - # - # source://webrick//lib/webrick/server.rb#82 - def listeners; end - - # The server logger. This is independent from the HTTP access log. - # - # source://webrick//lib/webrick/server.rb#71 - def logger; end - - # You must subclass GenericServer and implement \#run which accepts a TCP - # client socket - # - # source://webrick//lib/webrick/server.rb#244 - def run(sock); end - - # Shuts down the server and all listening sockets. New listeners must be - # provided to restart the server. - # - # source://webrick//lib/webrick/server.rb#234 - def shutdown; end - - # Starts the server and runs the +block+ for each connection. This method - # does not return until the server is stopped from a signal handler or - # another thread using #stop or #shutdown. - # - # If the block raises a subclass of StandardError the exception is logged - # and ignored. If an IOError or Errno::EBADF exception is raised the - # exception is ignored. If an Exception subclass is raised the exception - # is logged and re-raised which stops the server. - # - # To completely shut down a server call #shutdown from ensure: - # - # server = WEBrick::GenericServer.new - # # or WEBrick::HTTPServer.new - # - # begin - # server.start - # ensure - # server.shutdown - # end - # - # @raise [ServerError] - # - # source://webrick//lib/webrick/server.rb#154 - def start(&block); end - - # The server status. One of :Stop, :Running or :Shutdown - # - # source://webrick//lib/webrick/server.rb#61 - def status; end - - # Stops the server from accepting new connections. - # - # source://webrick//lib/webrick/server.rb#222 - def stop; end - - # Tokens control the number of outstanding clients. The - # :MaxClients configuration sets this. - # - # source://webrick//lib/webrick/server.rb#77 - def tokens; end - - private - - # Accepts a TCP client socket from the TCP server socket +svr+ and returns - # the client socket. - # - # source://webrick//lib/webrick/server.rb#256 - def accept_client(svr); end - - # source://webrick//lib/webrick/server.rb#347 - def alarm_shutdown_pipe; end - - # Calls the callback +callback_name+ from the configuration with +args+ - # - # source://webrick//lib/webrick/server.rb#334 - def call_callback(callback_name, *args); end - - # source://webrick//lib/webrick/server.rb#359 - def cleanup_listener; end - - # source://webrick//lib/webrick/server.rb#342 - def cleanup_shutdown_pipe(shutdown_pipe); end - - # source://webrick//lib/webrick/server.rb#338 - def setup_shutdown_pipe; end - - # Starts a server thread for the client socket +sock+ that runs the given - # +block+. - # - # Sets the socket to the :WEBrickSocket thread local variable - # in the thread. - # - # If any errors occur in the block they are logged and handled. - # - # source://webrick//lib/webrick/server.rb#288 - def start_thread(sock, &block); end -end - -# source://webrick//lib/webrick/htmlutils.rb#13 -module WEBrick::HTMLUtils - private - - # Escapes &, ", > and < in +string+ - # - # source://webrick//lib/webrick/htmlutils.rb#18 - def escape(string); end - - class << self - # Escapes &, ", > and < in +string+ - # - # source://webrick//lib/webrick/htmlutils.rb#18 - def escape(string); end - end -end - -# HTTPAuth provides both basic and digest authentication. -# -# To enable authentication for requests in WEBrick you will need a user -# database and an authenticator. To start, here's an Htpasswd database for -# use with a DigestAuth authenticator: -# -# config = { :Realm => 'DigestAuth example realm' } -# -# htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file' -# htpasswd.auth_type = WEBrick::HTTPAuth::DigestAuth -# htpasswd.set_passwd config[:Realm], 'username', 'password' -# htpasswd.flush -# -# The +:Realm+ is used to provide different access to different groups -# across several resources on a server. Typically you'll need only one -# realm for a server. -# -# This database can be used to create an authenticator: -# -# config[:UserDB] = htpasswd -# -# digest_auth = WEBrick::HTTPAuth::DigestAuth.new config -# -# To authenticate a request call #authenticate with a request and response -# object in a servlet: -# -# def do_GET req, res -# @authenticator.authenticate req, res -# end -# -# For digest authentication the authenticator must not be created every -# request, it must be passed in as an option via WEBrick::HTTPServer#mount. -# -# source://webrick//lib/webrick/httpauth/authenticator.rb#12 -module WEBrick::HTTPAuth - private - - # source://webrick//lib/webrick/httpauth.rb#57 - def _basic_auth(req, res, realm, req_field, res_field, err_type, block); end - - # Simple wrapper for providing basic authentication for a request. When - # called with a request +req+, response +res+, authentication +realm+ and - # +block+ the block will be called with a +username+ and +password+. If - # the block returns true the request is allowed to continue, otherwise an - # HTTPStatus::Unauthorized error is raised. - # - # source://webrick//lib/webrick/httpauth.rb#79 - def basic_auth(req, res, realm, &block); end - - # Simple wrapper for providing basic authentication for a proxied request. - # When called with a request +req+, response +res+, authentication +realm+ - # and +block+ the block will be called with a +username+ and +password+. - # If the block returns true the request is allowed to continue, otherwise - # an HTTPStatus::ProxyAuthenticationRequired error is raised. - # - # source://webrick//lib/webrick/httpauth.rb#91 - def proxy_basic_auth(req, res, realm, &block); end - - class << self - # source://webrick//lib/webrick/httpauth.rb#57 - def _basic_auth(req, res, realm, req_field, res_field, err_type, block); end - - # Simple wrapper for providing basic authentication for a request. When - # called with a request +req+, response +res+, authentication +realm+ and - # +block+ the block will be called with a +username+ and +password+. If - # the block returns true the request is allowed to continue, otherwise an - # HTTPStatus::Unauthorized error is raised. - # - # source://webrick//lib/webrick/httpauth.rb#79 - def basic_auth(req, res, realm, &block); end - - # Simple wrapper for providing basic authentication for a proxied request. - # When called with a request +req+, response +res+, authentication +realm+ - # and +block+ the block will be called with a +username+ and +password+. - # If the block returns true the request is allowed to continue, otherwise - # an HTTPStatus::ProxyAuthenticationRequired error is raised. - # - # source://webrick//lib/webrick/httpauth.rb#91 - def proxy_basic_auth(req, res, realm, &block); end - end -end - -# Module providing generic support for both Digest and Basic -# authentication schemes. -# -# source://webrick//lib/webrick/httpauth/authenticator.rb#18 -module WEBrick::HTTPAuth::Authenticator - # The logger for this authenticator - # - # source://webrick//lib/webrick/httpauth/authenticator.rb#43 - def logger; end - - # The realm this authenticator covers - # - # source://webrick//lib/webrick/httpauth/authenticator.rb#33 - def realm; end - - # The user database for this authenticator - # - # source://webrick//lib/webrick/httpauth/authenticator.rb#38 - def userdb; end - - private - - # Initializes the authenticator from +config+ - # - # source://webrick//lib/webrick/httpauth/authenticator.rb#52 - def check_init(config); end - - # Ensures +req+ has credentials that can be authenticated. - # - # source://webrick//lib/webrick/httpauth/authenticator.rb#72 - def check_scheme(req); end - - # source://webrick//lib/webrick/httpauth/authenticator.rb#91 - def error(fmt, *args); end - - # source://webrick//lib/webrick/httpauth/authenticator.rb#97 - def info(fmt, *args); end - - # source://webrick//lib/webrick/httpauth/authenticator.rb#85 - def log(meth, fmt, *args); end -end - -# source://webrick//lib/webrick/httpauth/authenticator.rb#23 -WEBrick::HTTPAuth::Authenticator::AuthException = WEBrick::HTTPStatus::Unauthorized - -# Basic Authentication for WEBrick -# -# Use this class to add basic authentication to a WEBrick servlet. -# -# Here is an example of how to set up a BasicAuth: -# -# config = { :Realm => 'BasicAuth example realm' } -# -# htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file', password_hash: :bcrypt -# htpasswd.set_passwd config[:Realm], 'username', 'password' -# htpasswd.flush -# -# config[:UserDB] = htpasswd -# -# basic_auth = WEBrick::HTTPAuth::BasicAuth.new config -# -# source://webrick//lib/webrick/httpauth/basicauth.rb#35 -class WEBrick::HTTPAuth::BasicAuth - include ::WEBrick::HTTPAuth::Authenticator - - # Creates a new BasicAuth instance. - # - # See WEBrick::Config::BasicAuth for default configuration entries - # - # You must supply the following configuration entries: - # - # :Realm:: The name of the realm being protected. - # :UserDB:: A database of usernames and passwords. - # A WEBrick::HTTPAuth::Htpasswd instance should be used. - # - # @return [BasicAuth] a new instance of BasicAuth - # - # source://webrick//lib/webrick/httpauth/basicauth.rb#61 - def initialize(config, default = T.unsafe(nil)); end - - # Authenticates a +req+ and returns a 401 Unauthorized using +res+ if - # the authentication was not correct. - # - # source://webrick//lib/webrick/httpauth/basicauth.rb#70 - def authenticate(req, res); end - - # Returns a challenge response which asks for authentication information - # - # @raise [@auth_exception] - # - # source://webrick//lib/webrick/httpauth/basicauth.rb#103 - def challenge(req, res); end - - # Returns the value of attribute logger. - # - # source://webrick//lib/webrick/httpauth/basicauth.rb#48 - def logger; end - - # Returns the value of attribute realm. - # - # source://webrick//lib/webrick/httpauth/basicauth.rb#48 - def realm; end - - # Returns the value of attribute userdb. - # - # source://webrick//lib/webrick/httpauth/basicauth.rb#48 - def userdb; end - - class << self - # Used by UserDB to create a basic password entry - # - # source://webrick//lib/webrick/httpauth/basicauth.rb#43 - def make_passwd(realm, user, pass); end - end -end - -# RFC 2617 Digest Access Authentication for WEBrick -# -# Use this class to add digest authentication to a WEBrick servlet. -# -# Here is an example of how to set up DigestAuth: -# -# config = { :Realm => 'DigestAuth example realm' } -# -# htdigest = WEBrick::HTTPAuth::Htdigest.new 'my_password_file' -# htdigest.set_passwd config[:Realm], 'username', 'password' -# htdigest.flush -# -# config[:UserDB] = htdigest -# -# digest_auth = WEBrick::HTTPAuth::DigestAuth.new config -# -# When using this as with a servlet be sure not to create a new DigestAuth -# object in the servlet's #initialize. By default WEBrick creates a new -# servlet instance for every request and the DigestAuth object must be -# used across requests. -# -# source://webrick//lib/webrick/httpauth/digestauth.rb#46 -class WEBrick::HTTPAuth::DigestAuth - include ::WEBrick::HTTPAuth::Authenticator - - # Creates a new DigestAuth instance. Be sure to use the same DigestAuth - # instance for multiple requests as it saves state between requests in - # order to perform authentication. - # - # See WEBrick::Config::DigestAuth for default configuration entries - # - # You must supply the following configuration entries: - # - # :Realm:: The name of the realm being protected. - # :UserDB:: A database of usernames and passwords. - # A WEBrick::HTTPAuth::Htdigest instance should be used. - # - # @return [DigestAuth] a new instance of DigestAuth - # - # source://webrick//lib/webrick/httpauth/digestauth.rb#87 - def initialize(config, default = T.unsafe(nil)); end - - # Digest authentication algorithm - # - # source://webrick//lib/webrick/httpauth/digestauth.rb#59 - def algorithm; end - - # Authenticates a +req+ and returns a 401 Unauthorized using +res+ if - # the authentication was not correct. - # - # source://webrick//lib/webrick/httpauth/digestauth.rb#121 - def authenticate(req, res); end - - # Returns a challenge response which asks for authentication information - # - # @raise [@auth_exception] - # - # source://webrick//lib/webrick/httpauth/digestauth.rb#134 - def challenge(req, res, stale = T.unsafe(nil)); end - - # Quality of protection. RFC 2617 defines "auth" and "auth-int" - # - # source://webrick//lib/webrick/httpauth/digestauth.rb#64 - def qop; end - - private - - # source://webrick//lib/webrick/httpauth/digestauth.rb#163 - def _authenticate(req, res); end - - # source://webrick//lib/webrick/httpauth/digestauth.rb#306 - def check_nonce(req, auth_req); end - - # source://webrick//lib/webrick/httpauth/digestauth.rb#349 - def check_opaque(opaque_struct, req, auth_req); end - - # source://webrick//lib/webrick/httpauth/digestauth.rb#365 - def check_uri(req, auth_req); end - - # source://webrick//lib/webrick/httpauth/digestauth.rb#299 - def generate_next_nonce(req); end - - # source://webrick//lib/webrick/httpauth/digestauth.rb#332 - def generate_opaque(req); end - - # source://webrick//lib/webrick/httpauth/digestauth.rb#376 - def hexdigest(*args); end - - # source://webrick//lib/webrick/httpauth/digestauth.rb#291 - def split_param_value(string); end - - class << self - # Used by UserDB to create a digest password entry - # - # source://webrick//lib/webrick/httpauth/digestauth.rb#69 - def make_passwd(realm, user, pass); end - end -end - -# Htdigest accesses apache-compatible digest password files. Passwords are -# matched to a realm where they are valid. For security, the path for a -# digest password database should be stored outside of the paths available -# to the HTTP server. -# -# Htdigest is intended for use with WEBrick::HTTPAuth::DigestAuth and -# stores passwords using cryptographic hashes. -# -# htpasswd = WEBrick::HTTPAuth::Htdigest.new 'my_password_file' -# htpasswd.set_passwd 'my realm', 'username', 'password' -# htpasswd.flush -# -# source://webrick//lib/webrick/httpauth/htdigest.rb#31 -class WEBrick::HTTPAuth::Htdigest - include ::WEBrick::HTTPAuth::UserDB - - # Open a digest password database at +path+ - # - # @return [Htdigest] a new instance of Htdigest - # - # source://webrick//lib/webrick/httpauth/htdigest.rb#37 - def initialize(path); end - - # Removes a password from the database for +user+ in +realm+. - # - # source://webrick//lib/webrick/httpauth/htdigest.rb#113 - def delete_passwd(realm, user); end - - # Iterate passwords in the database. - # - # source://webrick//lib/webrick/httpauth/htdigest.rb#122 - def each; end - - # Flush the password database. If +output+ is given the database will - # be written there instead of to the original path. - # - # source://webrick//lib/webrick/httpauth/htdigest.rb#72 - def flush(output = T.unsafe(nil)); end - - # Retrieves a password from the database for +user+ in +realm+. If - # +reload_db+ is true the database will be reloaded first. - # - # source://webrick//lib/webrick/httpauth/htdigest.rb#91 - def get_passwd(realm, user, reload_db); end - - # Reloads passwords from the database - # - # source://webrick//lib/webrick/httpauth/htdigest.rb#50 - def reload; end - - # Sets a password in the database for +user+ in +realm+ to +pass+. - # - # source://webrick//lib/webrick/httpauth/htdigest.rb#101 - def set_passwd(realm, user, pass); end -end - -# Htgroup accesses apache-compatible group files. Htgroup can be used to -# provide group-based authentication for users. Currently Htgroup is not -# directly integrated with any authenticators in WEBrick. For security, -# the path for a digest password database should be stored outside of the -# paths available to the HTTP server. -# -# Example: -# -# htgroup = WEBrick::HTTPAuth::Htgroup.new 'my_group_file' -# htgroup.add 'superheroes', %w[spiderman batman] -# -# htgroup.members('superheroes').include? 'magneto' # => false -# -# source://webrick//lib/webrick/httpauth/htgroup.rb#30 -class WEBrick::HTTPAuth::Htgroup - # Open a group database at +path+ - # - # @return [Htgroup] a new instance of Htgroup - # - # source://webrick//lib/webrick/httpauth/htgroup.rb#35 - def initialize(path); end - - # Add an Array of +members+ to +group+ - # - # source://webrick//lib/webrick/httpauth/htgroup.rb#92 - def add(group, members); end - - # Flush the group database. If +output+ is given the database will be - # written there instead of to the original path. - # - # source://webrick//lib/webrick/httpauth/htgroup.rb#64 - def flush(output = T.unsafe(nil)); end - - # Retrieve the list of members from +group+ - # - # source://webrick//lib/webrick/httpauth/htgroup.rb#84 - def members(group); end - - # Reload groups from the database - # - # source://webrick//lib/webrick/httpauth/htgroup.rb#46 - def reload; end -end - -# Htpasswd accesses apache-compatible password files. Passwords are -# matched to a realm where they are valid. For security, the path for a -# password database should be stored outside of the paths available to the -# HTTP server. -# -# Htpasswd is intended for use with WEBrick::HTTPAuth::BasicAuth. -# -# To create an Htpasswd database with a single user: -# -# htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file' -# htpasswd.set_passwd 'my realm', 'username', 'password' -# htpasswd.flush -# -# source://webrick//lib/webrick/httpauth/htpasswd.rb#32 -class WEBrick::HTTPAuth::Htpasswd - include ::WEBrick::HTTPAuth::UserDB - - # Open a password database at +path+ - # - # @return [Htpasswd] a new instance of Htpasswd - # - # source://webrick//lib/webrick/httpauth/htpasswd.rb#38 - def initialize(path, password_hash: T.unsafe(nil)); end - - # Removes a password from the database for +user+ in +realm+. - # - # source://webrick//lib/webrick/httpauth/htpasswd.rb#144 - def delete_passwd(realm, user); end - - # Iterate passwords in the database. - # - # source://webrick//lib/webrick/httpauth/htpasswd.rb#151 - def each; end - - # Flush the password database. If +output+ is given the database will - # be written there instead of to the original path. - # - # source://webrick//lib/webrick/httpauth/htpasswd.rb#103 - def flush(output = T.unsafe(nil)); end - - # Retrieves a password from the database for +user+ in +realm+. If - # +reload_db+ is true the database will be reloaded first. - # - # source://webrick//lib/webrick/httpauth/htpasswd.rb#122 - def get_passwd(realm, user, reload_db); end - - # Reload passwords from the database - # - # source://webrick//lib/webrick/httpauth/htpasswd.rb#68 - def reload; end - - # Sets a password in the database for +user+ in +realm+ to +pass+. - # - # source://webrick//lib/webrick/httpauth/htpasswd.rb#130 - def set_passwd(realm, user, pass); end -end - -# source://webrick//lib/webrick/httpauth/authenticator.rb#114 -WEBrick::HTTPAuth::ProxyAuthenticator::AuthException = WEBrick::HTTPStatus::ProxyAuthenticationRequired - -# source://webrick//lib/webrick/httpauth/authenticator.rb#113 -WEBrick::HTTPAuth::ProxyAuthenticator::ResponseInfoField = T.let(T.unsafe(nil), String) - -# Basic authentication for proxy servers. See BasicAuth for details. -# -# source://webrick//lib/webrick/httpauth/basicauth.rb#112 -class WEBrick::HTTPAuth::ProxyBasicAuth < ::WEBrick::HTTPAuth::BasicAuth - include ::WEBrick::HTTPAuth::ProxyAuthenticator -end - -# Digest authentication for proxy servers. See DigestAuth for details. -# -# source://webrick//lib/webrick/httpauth/digestauth.rb#386 -class WEBrick::HTTPAuth::ProxyDigestAuth < ::WEBrick::HTTPAuth::DigestAuth - include ::WEBrick::HTTPAuth::ProxyAuthenticator - - private - - # source://webrick//lib/webrick/httpauth/digestauth.rb#390 - def check_uri(req, auth_req); end -end - -# User database mixin for HTTPAuth. This mixin dispatches user record -# access to the underlying auth_type for this database. -# -# source://webrick//lib/webrick/httpauth/userdb.rb#18 -module WEBrick::HTTPAuth::UserDB - # The authentication type. - # - # WEBrick::HTTPAuth::BasicAuth or WEBrick::HTTPAuth::DigestAuth are - # built-in. - # - # source://webrick//lib/webrick/httpauth/userdb.rb#26 - def auth_type; end - - # The authentication type. - # - # WEBrick::HTTPAuth::BasicAuth or WEBrick::HTTPAuth::DigestAuth are - # built-in. - # - # source://webrick//lib/webrick/httpauth/userdb.rb#26 - def auth_type=(_arg0); end - - # Retrieves a password in +realm+ for +user+ for the auth_type of this - # database. +reload_db+ is a dummy value. - # - # source://webrick//lib/webrick/httpauth/userdb.rb#48 - def get_passwd(realm, user, reload_db = T.unsafe(nil)); end - - # Creates an obscured password in +realm+ with +user+ and +password+ - # using the auth_type of this database. - # - # source://webrick//lib/webrick/httpauth/userdb.rb#32 - def make_passwd(realm, user, pass); end - - # Sets a password in +realm+ with +user+ and +password+ for the - # auth_type of this database. - # - # source://webrick//lib/webrick/httpauth/userdb.rb#40 - def set_passwd(realm, user, pass); end -end - -# -- -# Adds SSL functionality to WEBrick::HTTPRequest -# -# source://webrick//lib/webrick/httprequest.rb#25 -class WEBrick::HTTPRequest - # Creates a new HTTP request. WEBrick::Config::HTTP is the default - # configuration. - # - # @return [HTTPRequest] a new instance of HTTPRequest - # - # source://webrick//lib/webrick/httprequest.rb#153 - def initialize(config); end - - # Retrieves +header_name+ - # - # source://webrick//lib/webrick/httprequest.rb#318 - def [](header_name); end - - # The Accept header value - # - # source://webrick//lib/webrick/httprequest.rb#100 - def accept; end - - # The Accept-Charset header value - # - # source://webrick//lib/webrick/httprequest.rb#105 - def accept_charset; end - - # The Accept-Encoding header value - # - # source://webrick//lib/webrick/httprequest.rb#110 - def accept_encoding; end - - # The Accept-Language header value - # - # source://webrick//lib/webrick/httprequest.rb#115 - def accept_language; end - - # The socket address of the server - # - # source://webrick//lib/webrick/httprequest.rb#127 - def addr; end - - # Hash of request attributes - # - # source://webrick//lib/webrick/httprequest.rb#137 - def attributes; end - - # Returns the request body. - # - # source://webrick//lib/webrick/httprequest.rb#255 - def body(&block); end - - # Prepares the HTTPRequest object for use as the - # source for IO.copy_stream - # - # source://webrick//lib/webrick/httprequest.rb#265 - def body_reader; end - - # The content-length header - # - # source://webrick//lib/webrick/httprequest.rb#304 - def content_length; end - - # The content-type header - # - # source://webrick//lib/webrick/httprequest.rb#311 - def content_type; end - - # Generate HTTP/1.1 100 continue response if the client expects it, - # otherwise does nothing. - # - # source://webrick//lib/webrick/httprequest.rb#245 - def continue; end - - # The parsed request cookies - # - # source://webrick//lib/webrick/httprequest.rb#95 - def cookies; end - - # Iterates over the request headers - # - # source://webrick//lib/webrick/httprequest.rb#328 - def each; end - - # Consumes any remaining body and updates keep-alive status - # - # source://webrick//lib/webrick/httprequest.rb#390 - def fixup; end - - # The parsed header of the request - # - # source://webrick//lib/webrick/httprequest.rb#90 - def header; end - - # The host this request is for - # - # source://webrick//lib/webrick/httprequest.rb#340 - def host; end - - # The HTTP version of the request - # - # source://webrick//lib/webrick/httprequest.rb#51 - def http_version; end - - # Is this a keep-alive connection? - # - # source://webrick//lib/webrick/httprequest.rb#142 - def keep_alive; end - - # Should the connection this request was made on be kept alive? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/httprequest.rb#375 - def keep_alive?; end - - # This method provides the metavariables defined by the revision 3 - # of "The WWW Common Gateway Interface Version 1.1" - # To browse the current document of CGI Version 1.1, see below: - # https://www.rfc-editor.org/rfc/rfc3875 - # - # source://webrick//lib/webrick/httprequest.rb#407 - def meta_vars; end - - # Parses a request from +socket+. This is called internally by - # WEBrick::HTTPServer. - # - # source://webrick//lib/webrick/httprequest.rb#193 - def parse(socket = T.unsafe(nil)); end - - # The request path - # - # source://webrick//lib/webrick/httprequest.rb#63 - def path; end - - # The path info (CGI variable) - # - # source://webrick//lib/webrick/httprequest.rb#73 - def path_info; end - - # The path info (CGI variable) - # - # source://webrick//lib/webrick/httprequest.rb#73 - def path_info=(_arg0); end - - # The socket address of the client - # - # source://webrick//lib/webrick/httprequest.rb#132 - def peeraddr; end - - # The port this request is for - # - # source://webrick//lib/webrick/httprequest.rb#347 - def port; end - - # Request query as a Hash - # - # source://webrick//lib/webrick/httprequest.rb#294 - def query; end - - # The query from the URI of the request - # - # source://webrick//lib/webrick/httprequest.rb#78 - def query_string; end - - # The query from the URI of the request - # - # source://webrick//lib/webrick/httprequest.rb#78 - def query_string=(_arg0); end - - # The raw header of the request - # - # source://webrick//lib/webrick/httprequest.rb#85 - def raw_header; end - - # for IO.copy_stream. - # - # source://webrick//lib/webrick/httprequest.rb#278 - def readpartial(size, buf = T.unsafe(nil)); end - - # The client's IP address - # - # source://webrick//lib/webrick/httprequest.rb#361 - def remote_ip; end - - # The complete request line such as: - # - # GET / HTTP/1.1 - # - # source://webrick//lib/webrick/httprequest.rb#36 - def request_line; end - - # The request method, GET, POST, PUT, etc. - # - # source://webrick//lib/webrick/httprequest.rb#41 - def request_method; end - - # The local time this request was received - # - # source://webrick//lib/webrick/httprequest.rb#147 - def request_time; end - - # The parsed URI of the request - # - # source://webrick//lib/webrick/httprequest.rb#58 - def request_uri; end - - # The script name (CGI variable) - # - # source://webrick//lib/webrick/httprequest.rb#68 - def script_name; end - - # The script name (CGI variable) - # - # source://webrick//lib/webrick/httprequest.rb#68 - def script_name=(_arg0); end - - # The server name this request is for - # - # source://webrick//lib/webrick/httprequest.rb#354 - def server_name; end - - # Is this an SSL request? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/httprequest.rb#368 - def ssl?; end - - # source://webrick//lib/webrick/httprequest.rb#379 - def to_s; end - - # The unparsed URI of the request - # - # source://webrick//lib/webrick/httprequest.rb#46 - def unparsed_uri; end - - # The remote user (CGI variable) - # - # source://webrick//lib/webrick/httprequest.rb#122 - def user; end - - # The remote user (CGI variable) - # - # source://webrick//lib/webrick/httprequest.rb#122 - def user=(_arg0); end - - private - - # source://webrick//lib/webrick/httprequest.rb#594 - def _read_data(io, method, *arg); end - - # source://webrick//lib/webrick/httprequest.rb#527 - def parse_host_request_line(host); end - - # source://webrick//lib/webrick/httprequest.rb#614 - def parse_query; end - - # source://webrick//lib/webrick/httprequest.rb#503 - def parse_uri(str, scheme = T.unsafe(nil)); end - - # source://webrick//lib/webrick/httprequest.rb#531 - def read_body(socket, block); end - - # source://webrick//lib/webrick/httprequest.rb#559 - def read_chunk_size(socket); end - - # source://webrick//lib/webrick/httprequest.rb#570 - def read_chunked(socket, block); end - - # source://webrick//lib/webrick/httprequest.rb#610 - def read_data(io, size); end - - # source://webrick//lib/webrick/httprequest.rb#471 - def read_header(socket); end - - # source://webrick//lib/webrick/httprequest.rb#606 - def read_line(io, size = T.unsafe(nil)); end - - # @raise [HTTPStatus::EOFError] - # - # source://webrick//lib/webrick/httprequest.rb#451 - def read_request_line(socket); end - - # It's said that all X-Forwarded-* headers will contain more than one - # (comma-separated) value if the original request already contained one of - # these headers. Since we could use these values as Host header, we choose - # the initial(first) value. (apr_table_mergen() adds new value after the - # existing value with ", " prefix) - # - # source://webrick//lib/webrick/httprequest.rb#642 - def setup_forwarded_info; end -end - -# source://webrick//lib/webrick/httprequest.rb#526 -WEBrick::HTTPRequest::HOST_PATTERN = T.let(T.unsafe(nil), Regexp) - -# same as Mongrel, Thin and Puma -# -# source://webrick//lib/webrick/httprequest.rb#449 -WEBrick::HTTPRequest::MAX_HEADER_LENGTH = T.let(T.unsafe(nil), Integer) - -# An HTTP response. This is filled in by the service or do_* methods of a -# WEBrick HTTP Servlet. -# -# source://webrick//lib/webrick/httpresponse.rb#24 -class WEBrick::HTTPResponse - # Creates a new HTTP response object. WEBrick::Config::HTTP is the - # default configuration. - # - # @return [HTTPResponse] a new instance of HTTPResponse - # - # source://webrick//lib/webrick/httpresponse.rb#117 - def initialize(config); end - - # Retrieves the response header +field+ - # - # source://webrick//lib/webrick/httpresponse.rb#155 - def [](field); end - - # Sets the response header +field+ to +value+ - # - # source://webrick//lib/webrick/httpresponse.rb#162 - def []=(field, value); end - - # Body may be: - # * a String; - # * an IO-like object that responds to +#read+ and +#readpartial+; - # * a Proc-like object that responds to +#call+. - # - # In the latter case, either #chunked= should be set to +true+, - # or header['content-length'] explicitly provided. - # Example: - # - # server.mount_proc '/' do |req, res| - # res.chunked = true - # # or - # # res.header['content-length'] = 10 - # res.body = proc { |out| out.write(Time.now.to_s) } - # end - # - # source://webrick//lib/webrick/httpresponse.rb#70 - def body; end - - # Body may be: - # * a String; - # * an IO-like object that responds to +#read+ and +#readpartial+; - # * a Proc-like object that responds to +#call+. - # - # In the latter case, either #chunked= should be set to +true+, - # or header['content-length'] explicitly provided. - # Example: - # - # server.mount_proc '/' do |req, res| - # res.chunked = true - # # or - # # res.header['content-length'] = 10 - # res.body = proc { |out| out.write(Time.now.to_s) } - # end - # - # source://webrick//lib/webrick/httpresponse.rb#70 - def body=(_arg0); end - - # Enables chunked transfer encoding. - # - # source://webrick//lib/webrick/httpresponse.rb#214 - def chunked=(val); end - - # Will this response body be returned using chunked transfer-encoding? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/httpresponse.rb#207 - def chunked?; end - - # Configuration for this response - # - # source://webrick//lib/webrick/httpresponse.rb#101 - def config; end - - # The content-length header - # - # source://webrick//lib/webrick/httpresponse.rb#170 - def content_length; end - - # Sets the content-length header to +len+ - # - # source://webrick//lib/webrick/httpresponse.rb#179 - def content_length=(len); end - - # The content-type header - # - # source://webrick//lib/webrick/httpresponse.rb#186 - def content_type; end - - # Sets the content-type header to +type+ - # - # source://webrick//lib/webrick/httpresponse.rb#193 - def content_type=(type); end - - # Response cookies - # - # source://webrick//lib/webrick/httpresponse.rb#46 - def cookies; end - - # Iterates over each header in the response - # - # source://webrick//lib/webrick/httpresponse.rb#200 - def each; end - - # Filename of the static file in this response. Only used by the - # FileHandler servlet. - # - # source://webrick//lib/webrick/httpresponse.rb#91 - def filename; end - - # Filename of the static file in this response. Only used by the - # FileHandler servlet. - # - # source://webrick//lib/webrick/httpresponse.rb#91 - def filename=(_arg0); end - - # Response header - # - # source://webrick//lib/webrick/httpresponse.rb#41 - def header; end - - # HTTP Response version - # - # source://webrick//lib/webrick/httpresponse.rb#31 - def http_version; end - - # Is this a keep-alive response? - # - # source://webrick//lib/webrick/httpresponse.rb#96 - def keep_alive; end - - # Is this a keep-alive response? - # - # source://webrick//lib/webrick/httpresponse.rb#96 - def keep_alive=(_arg0); end - - # Will this response's connection be kept alive? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/httpresponse.rb#221 - def keep_alive?; end - - # source://webrick//lib/webrick/httpresponse.rb#325 - def make_body_tempfile; end - - # Response reason phrase ("OK") - # - # source://webrick//lib/webrick/httpresponse.rb#51 - def reason_phrase; end - - # Response reason phrase ("OK") - # - # source://webrick//lib/webrick/httpresponse.rb#51 - def reason_phrase=(_arg0); end - - # source://webrick//lib/webrick/httpresponse.rb#343 - def remove_body_tempfile; end - - # Request HTTP version for this response - # - # source://webrick//lib/webrick/httpresponse.rb#85 - def request_http_version; end - - # Request HTTP version for this response - # - # source://webrick//lib/webrick/httpresponse.rb#85 - def request_http_version=(_arg0); end - - # Request method for this response - # - # source://webrick//lib/webrick/httpresponse.rb#75 - def request_method; end - - # Request method for this response - # - # source://webrick//lib/webrick/httpresponse.rb#75 - def request_method=(_arg0); end - - # Request URI for this response - # - # source://webrick//lib/webrick/httpresponse.rb#80 - def request_uri; end - - # Request URI for this response - # - # source://webrick//lib/webrick/httpresponse.rb#80 - def request_uri=(_arg0); end - - # Sends the body on +socket+ - # - # source://webrick//lib/webrick/httpresponse.rb#378 - def send_body(socket); end - - # Sends the headers on +socket+ - # - # source://webrick//lib/webrick/httpresponse.rb#355 - def send_header(socket); end - - # Sends the response on +socket+ - # - # source://webrick//lib/webrick/httpresponse.rb#238 - def send_response(socket); end - - # Bytes sent in this response - # - # source://webrick//lib/webrick/httpresponse.rb#106 - def sent_size; end - - # Creates an error page for exception +ex+ with an optional +backtrace+ - # - # source://webrick//lib/webrick/httpresponse.rb#405 - def set_error(ex, backtrace = T.unsafe(nil)); end - - # Redirects to +url+ with a WEBrick::HTTPStatus::Redirect +status+. - # - # Example: - # - # res.set_redirect WEBrick::HTTPStatus::TemporaryRedirect - # - # source://webrick//lib/webrick/httpresponse.rb#395 - def set_redirect(status, url); end - - # Sets up the headers for sending - # - # source://webrick//lib/webrick/httpresponse.rb#255 - def setup_header; end - - # Response status code (200) - # - # source://webrick//lib/webrick/httpresponse.rb#36 - def status; end - - # Sets the response's status to the +status+ code - # - # source://webrick//lib/webrick/httpresponse.rb#147 - def status=(status); end - - # The response's HTTP status line - # - # source://webrick//lib/webrick/httpresponse.rb#140 - def status_line; end - - # Set the response body proc as an streaming/upgrade response. - # - # source://webrick//lib/webrick/httpresponse.rb#111 - def upgrade; end - - # Sets the response to be a streaming/upgrade response. - # This will disable keep-alive and chunked transfer encoding. - # - # source://webrick//lib/webrick/httpresponse.rb#229 - def upgrade!(protocol); end - - # Set the response body proc as an streaming/upgrade response. - # - # source://webrick//lib/webrick/httpresponse.rb#111 - def upgrade=(_arg0); end - - private - - # preserved for compatibility with some 3rd-party handlers - # - # source://webrick//lib/webrick/httpresponse.rb#581 - def _write_data(socket, data); end - - # source://webrick//lib/webrick/httpresponse.rb#432 - def check_header(header_value); end - - # :stopdoc: - # - # source://webrick//lib/webrick/httpresponse.rb#443 - def error_body(backtrace, ex, host, port); end - - # source://webrick//lib/webrick/httpresponse.rb#473 - def send_body_io(socket); end - - # source://webrick//lib/webrick/httpresponse.rb#535 - def send_body_proc(socket); end - - # source://webrick//lib/webrick/httpresponse.rb#513 - def send_body_string(socket); end -end - -# source://webrick//lib/webrick/httpresponse.rb#555 -class WEBrick::HTTPResponse::ChunkedWrapper - # @return [ChunkedWrapper] a new instance of ChunkedWrapper - # - # source://webrick//lib/webrick/httpresponse.rb#556 - def initialize(socket, resp); end - - # source://webrick//lib/webrick/httpresponse.rb#574 - def <<(*buf); end - - # source://webrick//lib/webrick/httpresponse.rb#561 - def write(buf); end -end - -# An HTTP Server -# -# source://webrick//lib/webrick/httpserver.rb#27 -class WEBrick::HTTPServer < ::WEBrick::GenericServer - # Creates a new HTTP server according to +config+ - # - # An HTTP server uses the following attributes: - # - # :AccessLog:: An array of access logs. See WEBrick::AccessLog - # :BindAddress:: Local address for the server to bind to - # :DocumentRoot:: Root path to serve files from - # :DocumentRootOptions:: Options for the default HTTPServlet::FileHandler - # :HTTPVersion:: The HTTP version of this server - # :Port:: Port to listen on - # :RequestCallback:: Called with a request and response before each - # request is serviced. - # :RequestTimeout:: Maximum time to wait between requests - # :ServerAlias:: Array of alternate names for this server for virtual - # hosting - # :ServerName:: Name for this server for virtual hosting - # - # @return [HTTPServer] a new instance of HTTPServer - # - # source://webrick//lib/webrick/httpserver.rb#46 - def initialize(config = T.unsafe(nil), default = T.unsafe(nil)); end - - # Logs +req+ and +res+ in the access logs. +config+ is used for the - # server name. - # - # source://webrick//lib/webrick/httpserver.rb#220 - def access_log(config, req, res); end - - # Creates the HTTPRequest used when handling the HTTP - # request. Can be overridden by subclasses. - # - # source://webrick//lib/webrick/httpserver.rb#230 - def create_request(with_webrick_config); end - - # Creates the HTTPResponse used when handling the HTTP - # request. Can be overridden by subclasses. - # - # source://webrick//lib/webrick/httpserver.rb#237 - def create_response(with_webrick_config); end - - # The default OPTIONS request handler says GET, HEAD, POST and OPTIONS - # requests are allowed. - # - # source://webrick//lib/webrick/httpserver.rb#147 - def do_OPTIONS(req, res); end - - # Finds the appropriate virtual host to handle +req+ - # - # source://webrick//lib/webrick/httpserver.rb#207 - def lookup_server(req); end - - # Mounts +servlet+ on +dir+ passing +options+ to the servlet at creation - # time - # - # source://webrick//lib/webrick/httpserver.rb#155 - def mount(dir, servlet, *options); end - - # Mounts +proc+ or +block+ on +dir+ and calls it with a - # WEBrick::HTTPRequest and WEBrick::HTTPResponse - # - # @raise [HTTPServerError] - # - # source://webrick//lib/webrick/httpserver.rb#164 - def mount_proc(dir, proc = T.unsafe(nil), &block); end - - # Processes requests on +sock+ - # - # source://webrick//lib/webrick/httpserver.rb#69 - def run(sock); end - - # Finds a servlet for +path+ - # - # source://webrick//lib/webrick/httpserver.rb#182 - def search_servlet(path); end - - # Services +req+ and fills in +res+ - # - # @raise [HTTPStatus::NotFound] - # - # source://webrick//lib/webrick/httpserver.rb#125 - def service(req, res); end - - # Unmounts +dir+ - # - # source://webrick//lib/webrick/httpserver.rb#173 - def umount(dir); end - - # Unmounts +dir+ - # - # source://webrick//lib/webrick/httpserver.rb#173 - def unmount(dir); end - - # Adds +server+ as a virtual host. - # - # source://webrick//lib/webrick/httpserver.rb#193 - def virtual_host(server); end -end - -# Mount table for the path a servlet is mounted on in the directory space -# of the server. Users of WEBrick can only access this indirectly via -# WEBrick::HTTPServer#mount, WEBrick::HTTPServer#unmount and -# WEBrick::HTTPServer#search_servlet -# -# source://webrick//lib/webrick/httpserver.rb#247 -class WEBrick::HTTPServer::MountTable - # @return [MountTable] a new instance of MountTable - # - # source://webrick//lib/webrick/httpserver.rb#248 - def initialize; end - - # source://webrick//lib/webrick/httpserver.rb#253 - def [](dir); end - - # source://webrick//lib/webrick/httpserver.rb#258 - def []=(dir, val); end - - # source://webrick//lib/webrick/httpserver.rb#265 - def delete(dir); end - - # source://webrick//lib/webrick/httpserver.rb#272 - def scan(path); end - - private - - # source://webrick//lib/webrick/httpserver.rb#279 - def compile; end - - # source://webrick//lib/webrick/httpserver.rb#287 - def normalize(dir); end -end - -# AbstractServlet allows HTTP server modules to be reused across multiple -# servers and allows encapsulation of functionality. -# -# By default a servlet will respond to GET, HEAD (through an alias to GET) -# and OPTIONS requests. -# -# By default a new servlet is initialized for every request. A servlet -# instance can be reused by overriding ::get_instance in the -# AbstractServlet subclass. -# -# == A Simple Servlet -# -# class Simple < WEBrick::HTTPServlet::AbstractServlet -# def do_GET request, response -# status, content_type, body = do_stuff_with request -# -# response.status = status -# response['Content-Type'] = content_type -# response.body = body -# end -# -# def do_stuff_with request -# return 200, 'text/plain', 'you got a page' -# end -# end -# -# This servlet can be mounted on a server at a given path: -# -# server.mount '/simple', Simple -# -# == Servlet Configuration -# -# Servlets can be configured via initialize. The first argument is the -# HTTP server the servlet is being initialized for. -# -# class Configurable < Simple -# def initialize server, color, size -# super server -# @color = color -# @size = size -# end -# -# def do_stuff_with request -# content = "

Hello, World!" -# -# return 200, "text/html", content -# end -# end -# -# This servlet must be provided two arguments at mount time: -# -# server.mount '/configurable', Configurable, 'red', '2em' -# -# source://webrick//lib/webrick/httpservlet/abstract.rb#76 -class WEBrick::HTTPServlet::AbstractServlet - # Initializes a new servlet for +server+ using +options+ which are - # stored as-is in +@options+. +@logger+ is also provided. - # - # @return [AbstractServlet] a new instance of AbstractServlet - # - # source://webrick//lib/webrick/httpservlet/abstract.rb#91 - def initialize(server, *options); end - - # Raises a NotFound exception - # - # @raise [HTTPStatus::NotFound] - # - # source://webrick//lib/webrick/httpservlet/abstract.rb#115 - def do_GET(req, res); end - - # Dispatches to do_GET - # - # source://webrick//lib/webrick/httpservlet/abstract.rb#122 - def do_HEAD(req, res); end - - # Returns the allowed HTTP request methods - # - # source://webrick//lib/webrick/httpservlet/abstract.rb#129 - def do_OPTIONS(req, res); end - - # Dispatches to a +do_+ method based on +req+ if such a method is - # available. (+do_GET+ for a GET request). Raises a MethodNotAllowed - # exception if the method is not implemented. - # - # source://webrick//lib/webrick/httpservlet/abstract.rb#102 - def service(req, res); end - - private - - # Redirects to a path ending in / - # - # source://webrick//lib/webrick/httpservlet/abstract.rb#140 - def redirect_to_directory_uri(req, res); end - - class << self - # Factory for servlet instances that will handle a request from +server+ - # using +options+ from the mount point. By default a new servlet - # instance is created for every call. - # - # source://webrick//lib/webrick/httpservlet/abstract.rb#83 - def get_instance(server, *options); end - end -end - -# Servlet for handling CGI scripts -# -# Example: -# -# server.mount('/cgi/my_script', WEBrick::HTTPServlet::CGIHandler, -# '/path/to/my_script') -# -# source://webrick//lib/webrick/httpservlet/cgihandler.rb#28 -class WEBrick::HTTPServlet::CGIHandler < ::WEBrick::HTTPServlet::AbstractServlet - # Creates a new CGI script servlet for the script at +name+ - # - # @return [CGIHandler] a new instance of CGIHandler - # - # source://webrick//lib/webrick/httpservlet/cgihandler.rb#36 - def initialize(server, name); end - - # :stopdoc: - # - # @raise [HTTPStatus::InternalServerError] - # - # source://webrick//lib/webrick/httpservlet/cgihandler.rb#50 - def do_GET(req, res); end - - # :stopdoc: - # - # @raise [HTTPStatus::InternalServerError] - # - # source://webrick//lib/webrick/httpservlet/cgihandler.rb#50 - def do_POST(req, res); end -end - -# source://webrick//lib/webrick/httpservlet/cgihandler.rb#31 -WEBrick::HTTPServlet::CGIHandler::CGIRunnerArray = T.let(T.unsafe(nil), Array) - -# Servlet for serving a single file. You probably want to use the -# FileHandler servlet instead as it handles directories and fancy indexes. -# -# Example: -# -# server.mount('/my_page.txt', WEBrick::HTTPServlet::DefaultFileHandler, -# '/path/to/my_page.txt') -# -# This servlet handles If-Modified-Since and Range requests. -# -# source://webrick//lib/webrick/httpservlet/filehandler.rb#32 -class WEBrick::HTTPServlet::DefaultFileHandler < ::WEBrick::HTTPServlet::AbstractServlet - # Creates a DefaultFileHandler instance for the file at +local_path+. - # - # @return [DefaultFileHandler] a new instance of DefaultFileHandler - # - # source://webrick//lib/webrick/httpservlet/filehandler.rb#37 - def initialize(server, local_path); end - - # :stopdoc: - # - # source://webrick//lib/webrick/httpservlet/filehandler.rb#44 - def do_GET(req, res); end - - # source://webrick//lib/webrick/httpservlet/filehandler.rb#118 - def make_partial_content(req, res, filename, filesize); end - - # returns a lambda for webrick/httpresponse.rb send_body_proc - # - # source://webrick//lib/webrick/httpservlet/filehandler.rb#90 - def multipart_body(body, parts, boundary, mtype, filesize); end - - # @return [Boolean] - # - # source://webrick//lib/webrick/httpservlet/filehandler.rb#64 - def not_modified?(req, res, mtime, etag); end - - # source://webrick//lib/webrick/httpservlet/filehandler.rb#155 - def prepare_range(range, filesize); end -end - -# ERBHandler evaluates an ERB file and returns the result. This handler -# is automatically used if there are .rhtml files in a directory served by -# the FileHandler. -# -# ERBHandler supports GET and POST methods. -# -# The ERB file is evaluated with the local variables +servlet_request+ and -# +servlet_response+ which are a WEBrick::HTTPRequest and -# WEBrick::HTTPResponse respectively. -# -# Example .rhtml file: -# -# Request to <%= servlet_request.request_uri %> -# -# Query params <%= servlet_request.query.inspect %> -# -# source://webrick//lib/webrick/httpservlet/erbhandler.rb#36 -class WEBrick::HTTPServlet::ERBHandler < ::WEBrick::HTTPServlet::AbstractServlet - # Creates a new ERBHandler on +server+ that will evaluate and serve the - # ERB file +name+ - # - # @return [ERBHandler] a new instance of ERBHandler - # - # source://webrick//lib/webrick/httpservlet/erbhandler.rb#42 - def initialize(server, name); end - - # Handles GET requests - # - # source://webrick//lib/webrick/httpservlet/erbhandler.rb#50 - def do_GET(req, res); end - - # Handles GET requests - # - # Handles POST requests - # - # source://webrick//lib/webrick/httpservlet/erbhandler.rb#50 - def do_POST(req, res); end - - private - - # Evaluates +erb+ providing +servlet_request+ and +servlet_response+ as - # local variables. - # - # source://webrick//lib/webrick/httpservlet/erbhandler.rb#79 - def evaluate(erb, servlet_request, servlet_response); end -end - -# Serves a directory including fancy indexing and a variety of other -# options. -# -# Example: -# -# server.mount('/assets', WEBrick::HTTPServlet::FileHandler, -# '/path/to/assets') -# -# source://webrick//lib/webrick/httpservlet/filehandler.rb#175 -class WEBrick::HTTPServlet::FileHandler < ::WEBrick::HTTPServlet::AbstractServlet - # Creates a FileHandler servlet on +server+ that serves files starting - # at directory +root+ - # - # +options+ may be a Hash containing keys from - # WEBrick::Config::FileHandler or +true+ or +false+. - # - # If +options+ is true or false then +:FancyIndexing+ is enabled or - # disabled respectively. - # - # @return [FileHandler] a new instance of FileHandler - # - # source://webrick//lib/webrick/httpservlet/filehandler.rb#203 - def initialize(server, root, options = T.unsafe(nil), default = T.unsafe(nil)); end - - # source://webrick//lib/webrick/httpservlet/filehandler.rb#245 - def do_GET(req, res); end - - # source://webrick//lib/webrick/httpservlet/filehandler.rb#257 - def do_OPTIONS(req, res); end - - # source://webrick//lib/webrick/httpservlet/filehandler.rb#251 - def do_POST(req, res); end - - # source://webrick//lib/webrick/httpservlet/filehandler.rb#224 - def service(req, res); end - - # :stopdoc: - # - # source://webrick//lib/webrick/httpservlet/filehandler.rb#215 - def set_filesystem_encoding(str); end - - private - - # source://webrick//lib/webrick/httpservlet/filehandler.rb#416 - def call_callback(callback_name, req, res); end - - # source://webrick//lib/webrick/httpservlet/filehandler.rb#369 - def check_filename(req, res, name); end - - # @raise [HTTPStatus::NotFound] - # - # source://webrick//lib/webrick/httpservlet/filehandler.rb#309 - def exec_handler(req, res); end - - # source://webrick//lib/webrick/httpservlet/filehandler.rb#322 - def get_handler(req, res); end - - # @return [Boolean] - # - # source://webrick//lib/webrick/httpservlet/filehandler.rb#428 - def nondisclosure_name?(name); end - - # source://webrick//lib/webrick/httpservlet/filehandler.rb#286 - def prevent_directory_traversal(req, res); end - - # source://webrick//lib/webrick/httpservlet/filehandler.rb#394 - def search_file(req, res, basename); end - - # source://webrick//lib/webrick/httpservlet/filehandler.rb#385 - def search_index_file(req, res); end - - # source://webrick//lib/webrick/httpservlet/filehandler.rb#437 - def set_dir_list(req, res); end - - # source://webrick//lib/webrick/httpservlet/filehandler.rb#335 - def set_filename(req, res); end - - # source://webrick//lib/webrick/httpservlet/filehandler.rb#376 - def shift_path_info(req, res, path_info, base = T.unsafe(nil)); end - - # @return [Boolean] - # - # source://webrick//lib/webrick/httpservlet/filehandler.rb#277 - def trailing_pathsep?(path); end - - # @return [Boolean] - # - # source://webrick//lib/webrick/httpservlet/filehandler.rb#422 - def windows_ambiguous_name?(name); end - - class << self - # Allow custom handling of requests for files with +suffix+ by class - # +handler+ - # - # source://webrick//lib/webrick/httpservlet/filehandler.rb#182 - def add_handler(suffix, handler); end - - # Remove custom handling of requests for files with +suffix+ - # - # source://webrick//lib/webrick/httpservlet/filehandler.rb#189 - def remove_handler(suffix); end - end -end - -# Mounts a proc at a path that accepts a request and response. -# -# Instead of mounting this servlet with WEBrick::HTTPServer#mount use -# WEBrick::HTTPServer#mount_proc: -# -# server.mount_proc '/' do |req, res| -# res.body = 'it worked!' -# res.status = 200 -# end -# -# source://webrick//lib/webrick/httpservlet/prochandler.rb#28 -class WEBrick::HTTPServlet::ProcHandler < ::WEBrick::HTTPServlet::AbstractServlet - # @return [ProcHandler] a new instance of ProcHandler - # - # source://webrick//lib/webrick/httpservlet/prochandler.rb#34 - def initialize(proc); end - - # source://webrick//lib/webrick/httpservlet/prochandler.rb#38 - def do_GET(request, response); end - - # source://webrick//lib/webrick/httpservlet/prochandler.rb#38 - def do_POST(request, response); end - - # source://webrick//lib/webrick/httpservlet/prochandler.rb#38 - def do_PUT(request, response); end - - # :stopdoc: - # - # source://webrick//lib/webrick/httpservlet/prochandler.rb#30 - def get_instance(server, *options); end -end - -# This module is used to manager HTTP status codes. -# -# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for more -# information. -# -# source://webrick//lib/webrick/httpstatus.rb#21 -module WEBrick::HTTPStatus - private - - # Is +code+ a client error status? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/httpstatus.rb#170 - def client_error?(code); end - - # Is +code+ an error status? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/httpstatus.rb#164 - def error?(code); end - - # Is +code+ an informational status? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/httpstatus.rb#146 - def info?(code); end - - # Returns the description corresponding to the HTTP status +code+ - # - # WEBrick::HTTPStatus.reason_phrase 404 - # => "Not Found" - # - # source://webrick//lib/webrick/httpstatus.rb#140 - def reason_phrase(code); end - - # Is +code+ a redirection status? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/httpstatus.rb#158 - def redirect?(code); end - - # Is +code+ a server error status? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/httpstatus.rb#176 - def server_error?(code); end - - # Is +code+ a successful status? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/httpstatus.rb#152 - def success?(code); end - - class << self - # Returns the status class corresponding to +code+ - # - # WEBrick::HTTPStatus[302] - # => WEBrick::HTTPStatus::NotFound - # - # source://webrick//lib/webrick/httpstatus.rb#186 - def [](code); end - - # Is +code+ a client error status? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/httpstatus.rb#170 - def client_error?(code); end - - # Is +code+ an error status? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/httpstatus.rb#164 - def error?(code); end - - # Is +code+ an informational status? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/httpstatus.rb#146 - def info?(code); end - - # Returns the description corresponding to the HTTP status +code+ - # - # WEBrick::HTTPStatus.reason_phrase 404 - # => "Not Found" - # - # source://webrick//lib/webrick/httpstatus.rb#140 - def reason_phrase(code); end - - # Is +code+ a redirection status? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/httpstatus.rb#158 - def redirect?(code); end - - # Is +code+ a server error status? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/httpstatus.rb#176 - def server_error?(code); end - - # Is +code+ a successful status? - # - # @return [Boolean] - # - # source://webrick//lib/webrick/httpstatus.rb#152 - def success?(code); end - end -end - -# Root of the HTTP status class hierarchy -# -# source://webrick//lib/webrick/httpstatus.rb#25 -class WEBrick::HTTPStatus::Status < ::StandardError - # Returns the HTTP status code - # - # source://webrick//lib/webrick/httpstatus.rb#31 - def code; end - - # Returns the HTTP status description - # - # source://webrick//lib/webrick/httpstatus.rb#34 - def reason_phrase; end - - # Returns the HTTP status code - # - # source://webrick//lib/webrick/httpstatus.rb#31 - def to_i; end - - class << self - # source://webrick//lib/webrick/httpstatus.rb#27 - def code; end - - # source://webrick//lib/webrick/httpstatus.rb#27 - def reason_phrase; end - end -end - -# HTTPUtils provides utility methods for working with the HTTP protocol. -# -# This module is generally used internally by WEBrick -# -# source://webrick//lib/webrick/httputils.rb#25 -module WEBrick::HTTPUtils - private - - # source://webrick//lib/webrick/httputils.rb#474 - def _escape(str, regex); end - - # :stopdoc: - # - # source://webrick//lib/webrick/httputils.rb#472 - def _make_regex(str); end - - # source://webrick//lib/webrick/httputils.rb#473 - def _make_regex!(str); end - - # source://webrick//lib/webrick/httputils.rb#480 - def _unescape(str, regex); end - - # Removes quotes and escapes from +str+ - # - # source://webrick//lib/webrick/httputils.rb#254 - def dequote(str); end - - # Escapes HTTP reserved and unwise characters in +str+ - # - # source://webrick//lib/webrick/httputils.rb#498 - def escape(str); end - - # Escapes 8 bit characters in +str+ - # - # source://webrick//lib/webrick/httputils.rb#539 - def escape8bit(str); end - - # Escapes form reserved characters in +str+ - # - # source://webrick//lib/webrick/httputils.rb#512 - def escape_form(str); end - - # Escapes path +str+ - # - # source://webrick//lib/webrick/httputils.rb#528 - def escape_path(str); end - - # Loads Apache-compatible mime.types in +file+. - # - # source://webrick//lib/webrick/httputils.rb#122 - def load_mime_types(file); end - - # Returns the mime type of +filename+ from the list in +mime_tab+. If no - # mime type was found application/octet-stream is returned. - # - # source://webrick//lib/webrick/httputils.rb#144 - def mime_type(filename, mime_tab); end - - # Normalizes a request path. Raises an exception if the path cannot be - # normalized. - # - # source://webrick//lib/webrick/httputils.rb#31 - def normalize_path(path); end - - # Parses form data in +io+ with the given +boundary+ - # - # source://webrick//lib/webrick/httputils.rb#426 - def parse_form_data(io, boundary); end - - # source://webrick//lib/webrick/httputils.rb#171 - def parse_header(raw); end - - # Parses the query component of a URI in +str+ - # - # source://webrick//lib/webrick/httputils.rb#402 - def parse_query(str); end - - # Parses q values in +value+ as used in Accept headers. - # - # source://webrick//lib/webrick/httputils.rb#233 - def parse_qvalues(value); end - - # Parses a Range header value +ranges_specifier+ - # - # source://webrick//lib/webrick/httputils.rb#215 - def parse_range_header(ranges_specifier); end - - # Quotes and escapes quotes in +str+ - # - # source://webrick//lib/webrick/httputils.rb#264 - def quote(str); end - - # Splits a header value +str+ according to HTTP specification. - # - # source://webrick//lib/webrick/httputils.rb#206 - def split_header_value(str); end - - # Unescapes HTTP reserved and unwise characters in +str+ - # - # source://webrick//lib/webrick/httputils.rb#505 - def unescape(str); end - - # Unescapes form reserved characters in +str+ - # - # source://webrick//lib/webrick/httputils.rb#521 - def unescape_form(str); end - - class << self - # source://webrick//lib/webrick/httputils.rb#474 - def _escape(str, regex); end - - # :stopdoc: - # - # source://webrick//lib/webrick/httputils.rb#472 - def _make_regex(str); end - - # source://webrick//lib/webrick/httputils.rb#473 - def _make_regex!(str); end - - # source://webrick//lib/webrick/httputils.rb#480 - def _unescape(str, regex); end - - # Removes quotes and escapes from +str+ - # - # source://webrick//lib/webrick/httputils.rb#254 - def dequote(str); end - - # Escapes HTTP reserved and unwise characters in +str+ - # - # source://webrick//lib/webrick/httputils.rb#498 - def escape(str); end - - # Escapes 8 bit characters in +str+ - # - # source://webrick//lib/webrick/httputils.rb#539 - def escape8bit(str); end - - # Escapes form reserved characters in +str+ - # - # source://webrick//lib/webrick/httputils.rb#512 - def escape_form(str); end - - # Escapes path +str+ - # - # source://webrick//lib/webrick/httputils.rb#528 - def escape_path(str); end - - # Loads Apache-compatible mime.types in +file+. - # - # source://webrick//lib/webrick/httputils.rb#122 - def load_mime_types(file); end - - # Returns the mime type of +filename+ from the list in +mime_tab+. If no - # mime type was found application/octet-stream is returned. - # - # source://webrick//lib/webrick/httputils.rb#144 - def mime_type(filename, mime_tab); end - - # Normalizes a request path. Raises an exception if the path cannot be - # normalized. - # - # source://webrick//lib/webrick/httputils.rb#31 - def normalize_path(path); end - - # Parses form data in +io+ with the given +boundary+ - # - # source://webrick//lib/webrick/httputils.rb#426 - def parse_form_data(io, boundary); end - - # source://webrick//lib/webrick/httputils.rb#171 - def parse_header(raw); end - - # Parses the query component of a URI in +str+ - # - # source://webrick//lib/webrick/httputils.rb#402 - def parse_query(str); end - - # Parses q values in +value+ as used in Accept headers. - # - # source://webrick//lib/webrick/httputils.rb#233 - def parse_qvalues(value); end - - # Parses a Range header value +ranges_specifier+ - # - # source://webrick//lib/webrick/httputils.rb#215 - def parse_range_header(ranges_specifier); end - - # Quotes and escapes quotes in +str+ - # - # source://webrick//lib/webrick/httputils.rb#264 - def quote(str); end - - # Splits a header value +str+ according to HTTP specification. - # - # source://webrick//lib/webrick/httputils.rb#206 - def split_header_value(str); end - - # Unescapes HTTP reserved and unwise characters in +str+ - # - # source://webrick//lib/webrick/httputils.rb#505 - def unescape(str); end - - # Unescapes form reserved characters in +str+ - # - # source://webrick//lib/webrick/httputils.rb#521 - def unescape_form(str); end - end -end - -# source://webrick//lib/webrick/httputils.rb#161 -class WEBrick::HTTPUtils::CookieHeader < ::Array - # source://webrick//lib/webrick/httputils.rb#162 - def join(separator = T.unsafe(nil)); end -end - -# Stores multipart form data. FormData objects are created when -# WEBrick::HTTPUtils.parse_form_data is called. -# -# source://webrick//lib/webrick/httputils.rb#273 -class WEBrick::HTTPUtils::FormData < ::String - # Creates a new FormData object. - # - # +args+ is an Array of form data entries. One FormData will be created - # for each entry. - # - # This is called by WEBrick::HTTPUtils.parse_form_data for you - # - # @return [FormData] a new instance of FormData - # - # source://webrick//lib/webrick/httputils.rb#298 - def initialize(*args); end - - # Adds +str+ to this FormData which may be the body, a header or a - # header entry. - # - # This is called by WEBrick::HTTPUtils.parse_form_data for you - # - # source://webrick//lib/webrick/httputils.rb#331 - def <<(str); end - - # Retrieves the header at the first entry in +key+ - # - # source://webrick//lib/webrick/httputils.rb#317 - def [](*key); end - - # Adds +data+ at the end of the chain of entries - # - # This is called by WEBrick::HTTPUtils.parse_form_data for you. - # - # source://webrick//lib/webrick/httputils.rb#351 - def append_data(data); end - - # Yields each entry in this FormData - # - # source://webrick//lib/webrick/httputils.rb#366 - def each_data; end - - # The filename of the form data part - # - # source://webrick//lib/webrick/httputils.rb#285 - def filename; end - - # The filename of the form data part - # - # source://webrick//lib/webrick/httputils.rb#285 - def filename=(_arg0); end - - # Returns all the FormData as an Array - # - # source://webrick//lib/webrick/httputils.rb#378 - def list; end - - # The name of the form data part - # - # source://webrick//lib/webrick/httputils.rb#280 - def name; end - - # The name of the form data part - # - # source://webrick//lib/webrick/httputils.rb#280 - def name=(_arg0); end - - # source://webrick//lib/webrick/httputils.rb#287 - def next_data=(_arg0); end - - # Returns all the FormData as an Array - # - # A FormData will behave like an Array - # - # source://webrick//lib/webrick/httputils.rb#378 - def to_ary; end - - # This FormData's body - # - # source://webrick//lib/webrick/httputils.rb#394 - def to_s; end - - protected - - # source://webrick//lib/webrick/httputils.rb#287 - def next_data; end -end - -# source://webrick//lib/webrick/httputils.rb#167 -WEBrick::HTTPUtils::HEADER_CLASSES = T.let(T.unsafe(nil), Hash) - -# Parses an HTTP header +raw+ into a hash of header fields with an Array -# of values. -# -# source://webrick//lib/webrick/httputils.rb#155 -class WEBrick::HTTPUtils::SplitHeader < ::Array - # source://webrick//lib/webrick/httputils.rb#156 - def join(separator = T.unsafe(nil)); end -end - -# Represents an HTTP protocol version -# -# source://webrick//lib/webrick/httpversion.rb#16 -class WEBrick::HTTPVersion - include ::Comparable - - # Creates a new HTTPVersion from +version+. - # - # @return [HTTPVersion] a new instance of HTTPVersion - # - # source://webrick//lib/webrick/httpversion.rb#39 - def initialize(version); end - - # Compares this version with +other+ according to the HTTP specification - # rules. - # - # source://webrick//lib/webrick/httpversion.rb#58 - def <=>(other); end - - # The major protocol version number - # - # source://webrick//lib/webrick/httpversion.rb#22 - def major; end - - # The major protocol version number - # - # source://webrick//lib/webrick/httpversion.rb#22 - def major=(_arg0); end - - # The minor protocol version number - # - # source://webrick//lib/webrick/httpversion.rb#27 - def minor; end - - # The minor protocol version number - # - # source://webrick//lib/webrick/httpversion.rb#27 - def minor=(_arg0); end - - # The HTTP version as show in the HTTP request and response. For example, - # "1.1" - # - # source://webrick//lib/webrick/httpversion.rb#72 - def to_s; end - - class << self - # Converts +version+ into an HTTPVersion - # - # source://webrick//lib/webrick/httpversion.rb#32 - def convert(version); end - end -end - -# A logging class that prepends a timestamp to each message. -# -# source://webrick//lib/webrick/log.rb#134 -class WEBrick::Log < ::WEBrick::BasicLog - # Same as BasicLog#initialize - # - # You can set the timestamp format through #time_format - # - # @return [Log] a new instance of Log - # - # source://webrick//lib/webrick/log.rb#143 - def initialize(log_file = T.unsafe(nil), level = T.unsafe(nil)); end - - # Same as BasicLog#log - # - # source://webrick//lib/webrick/log.rb#150 - def log(level, data); end - - # Format of the timestamp which is applied to each logged line. The - # default is "[%Y-%m-%d %H:%M:%S]" - # - # source://webrick//lib/webrick/log.rb#137 - def time_format; end - - # Format of the timestamp which is applied to each logged line. The - # default is "[%Y-%m-%d %H:%M:%S]" - # - # source://webrick//lib/webrick/log.rb#137 - def time_format=(_arg0); end -end - -# Base server class -# -# source://webrick//lib/webrick/server.rb#26 -class WEBrick::SimpleServer - class << self - # A SimpleServer only yields when you start it - # - # source://webrick//lib/webrick/server.rb#31 - def start; end - end -end - -# source://webrick//lib/webrick/utils.rb#17 -module WEBrick::Utils - private - - # Creates TCP server sockets bound to +address+:+port+ and returns them. - # - # It will create IPV4 and IPV6 sockets on all interfaces. - # - # source://webrick//lib/webrick/utils.rb#56 - def create_listeners(address, port); end - - # The server hostname - # - # source://webrick//lib/webrick/utils.rb#47 - def getservername; end - - # Generates a random string of length +len+ - # - # source://webrick//lib/webrick/utils.rb#79 - def random_string(len); end - - # Sets the close on exec flag for +io+ - # - # source://webrick//lib/webrick/utils.rb#27 - def set_close_on_exec(io); end - - # Sets IO operations on +io+ to be non-blocking - # - # source://webrick//lib/webrick/utils.rb#20 - def set_non_blocking(io); end - - # Changes the process's uid and gid to the ones of +user+ - # - # source://webrick//lib/webrick/utils.rb#34 - def su(user); end - - # Executes the passed block and raises +exception+ if execution takes more - # than +seconds+. - # - # If +seconds+ is zero or nil, simply executes the block - # - # source://webrick//lib/webrick/utils.rb#253 - def timeout(seconds, exception = T.unsafe(nil)); end - - class << self - # Creates TCP server sockets bound to +address+:+port+ and returns them. - # - # It will create IPV4 and IPV6 sockets on all interfaces. - # - # source://webrick//lib/webrick/utils.rb#56 - def create_listeners(address, port); end - - # The server hostname - # - # source://webrick//lib/webrick/utils.rb#47 - def getservername; end - - # Generates a random string of length +len+ - # - # source://webrick//lib/webrick/utils.rb#79 - def random_string(len); end - - # Sets the close on exec flag for +io+ - # - # source://webrick//lib/webrick/utils.rb#27 - def set_close_on_exec(io); end - - # Sets IO operations on +io+ to be non-blocking - # - # source://webrick//lib/webrick/utils.rb#20 - def set_non_blocking(io); end - - # Changes the process's uid and gid to the ones of +user+ - # - # source://webrick//lib/webrick/utils.rb#34 - def su(user); end - - # Executes the passed block and raises +exception+ if execution takes more - # than +seconds+. - # - # If +seconds+ is zero or nil, simply executes the block - # - # source://webrick//lib/webrick/utils.rb#253 - def timeout(seconds, exception = T.unsafe(nil)); end - end -end - -# Class used to manage timeout handlers across multiple threads. -# -# Timeout handlers should be managed by using the class methods which are -# synchronized. -# -# id = TimeoutHandler.register(10, Timeout::Error) -# begin -# sleep 20 -# puts 'foo' -# ensure -# TimeoutHandler.cancel(id) -# end -# -# will raise Timeout::Error -# -# id = TimeoutHandler.register(10, Timeout::Error) -# begin -# sleep 5 -# puts 'foo' -# ensure -# TimeoutHandler.cancel(id) -# end -# -# will print 'foo' -# -# source://webrick//lib/webrick/utils.rb#118 -class WEBrick::Utils::TimeoutHandler - include ::Singleton::SingletonInstanceMethods - include ::Singleton - extend ::Singleton::SingletonClassMethods - - # Creates a new TimeoutHandler. You should use ::register and ::cancel - # instead of creating the timeout handler directly. - # - # @return [TimeoutHandler] a new instance of TimeoutHandler - # - # source://webrick//lib/webrick/utils.rb#148 - def initialize; end - - # Cancels the timeout handler +id+ - # - # source://webrick//lib/webrick/utils.rb#226 - def cancel(thread, id); end - - # Interrupts the timeout handler +id+ and raises +exception+ - # - # source://webrick//lib/webrick/utils.rb#203 - def interrupt(thread, id, exception); end - - # Registers a new timeout handler - # - # +time+:: Timeout in seconds - # +exception+:: Exception to raise when timeout elapsed - # - # source://webrick//lib/webrick/utils.rb#214 - def register(thread, time, exception); end - - # source://webrick//lib/webrick/utils.rb#240 - def terminate; end - - private - - # source://webrick//lib/webrick/utils.rb#158 - def watch; end - - # source://webrick//lib/webrick/utils.rb#193 - def watcher; end - - class << self - # Cancels the timeout handler +id+ - # - # source://webrick//lib/webrick/utils.rb#137 - def cancel(id); end - - # Registers a new timeout handler - # - # +time+:: Timeout in seconds - # +exception+:: Exception to raise when timeout elapsed - # - # source://webrick//lib/webrick/utils.rb#130 - def register(seconds, exception); end - - # source://webrick//lib/webrick/utils.rb#141 - def terminate; end - end -end diff --git a/sorbet/tapioca/require.rb b/sorbet/tapioca/require.rb index 77ce9e4742..7573e61e7a 100644 --- a/sorbet/tapioca/require.rb +++ b/sorbet/tapioca/require.rb @@ -10,7 +10,6 @@ require "language_server-protocol" require "prism" -require "webrick" require "prism/visitor" require "mocha/minitest" require "rubocop/minitest/assert_offense" diff --git a/test/mcp_server_test.rb b/test/mcp_server_test.rb index 4c6dd65306..a4e89d1a40 100644 --- a/test/mcp_server_test.rb +++ b/test/mcp_server_test.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true require "test_helper" -require "net/http" +require "socket" module RubyLsp class MCPServerTest < Minitest::Test @@ -13,7 +13,9 @@ def setup # Initialize the index with Ruby core - this is essential for method resolution! RubyIndexer::RBSIndexer.new(@index).index_ruby_core @mcp_server = MCPServer.new(@global_state) - @mcp_server.start + capture_io do + @mcp_server.start + end @mcp_port = @mcp_server.instance_variable_get(:@port) @@ -213,18 +215,12 @@ def test_invalid_tool_name end def test_server_handles_malformed_json - uri = URI("http://127.0.0.1:#{@mcp_port}/mcp") - - http = Net::HTTP.new(uri.host, uri.port) - request = Net::HTTP::Post.new(uri.path) - request["Content-Type"] = "application/json" - request.body = "{ invalid json" + socket = TCPSocket.new("127.0.0.1", @mcp_port) + socket.puts("{ invalid json") + response_line = socket.gets #: as !nil + socket.close - response = http.request(request) - - assert_equal("200", response.code) - - response_data = JSON.parse(response.body) + response_data = JSON.parse(response_line) assert_equal("2.0", response_data["jsonrpc"]) assert(response_data["error"]) end @@ -232,29 +228,27 @@ def test_server_handles_malformed_json private def send_mcp_request(method, params) - uri = URI("http://127.0.0.1:#{@mcp_port}/mcp") - - http = Net::HTTP.new(uri.host, uri.port) - request = Net::HTTP::Post.new(uri.path) - request["Content-Type"] = "application/json" - request.body = { + request_data = { jsonrpc: "2.0", id: 1, method: method, params: params, }.to_json - response = http.request(request) + socket = TCPSocket.new("127.0.0.1", @mcp_port) + socket.puts(request_data) + response_line = socket.gets + socket.close - if response.code == "200" - response_data = JSON.parse(response.body) + if response_line + response_data = JSON.parse(response_line) if response_data["error"] raise "MCP request failed: #{response_data["error"]}" end response_data["result"] else - raise "HTTP request failed: #{response.code} #{response.body}" + raise "No response received from TCP server" end end end diff --git a/vscode/ruby-mcp-bridge b/vscode/ruby-mcp-bridge index 777c5a3c5b..7729ac401f 100755 --- a/vscode/ruby-mcp-bridge +++ b/vscode/ruby-mcp-bridge @@ -20,18 +20,15 @@ fi # Read port from file PORT=$(cat "$PORT_FILE") -echo "Bridge started using port $PORT" >> "$LOG_FILE" +echo "Bridge started using TCP port $PORT" >> "$LOG_FILE" while IFS= read -r line; do echo "================================" >> "$LOG_FILE" echo "Input JSON: $line" >> "$LOG_FILE" - # Send HTTP request to the WEBrick server - response=$(curl -s -X POST -H "Content-Type: application/json" \ - -H "Content-Length: ${#line}" \ - --data "$line" \ - "http://127.0.0.1:$PORT/mcp" 2>>"$LOG_FILE") || { - echo "HTTP request failed" >> "$LOG_FILE" + # Send JSON-RPC request directly over TCP + response=$(echo "$line" | nc 127.0.0.1 "$PORT" 2>>"$LOG_FILE") || { + echo "TCP connection failed" >> "$LOG_FILE" continue }