searxng 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2b8849ed3269f41fc39d8b6274f8b0ae92ade2649dd1f6e5111b1799fec375e5
4
+ data.tar.gz: cd78466743cbad58a93d2453f2128e7bfb2f139a05c84eba0c984166da662e2f
5
+ SHA512:
6
+ metadata.gz: 38818edbabaf94b55bca1e43cbee38fb8bf7d54319d1b2fe3b56a10653f33152011697dd01e9135d1e05fc23f07629a716578dce791b55addc97ca3f471afd92
7
+ data.tar.gz: 4a77e1ba98532c75d7da8c5e3da80c5cb53127488c094fa8dfc331de5285afb4a78e5278f332aa92e27d8e489a1273e8e39eea1027d4643988869c9bdd75b610
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial release.
6
+ - `Searxng::Client` for SearXNG `/search` JSON API (results, infoboxes, suggestions, answers, corrections).
7
+ - CLI: `searxng search QUERY` and `searxng serve` (MCP server).
8
+ - MCP tool `searxng_web_search` (query, pageno, time_range, language, safesearch).
9
+ - Configuration via SEARXNG_URL, SEARXNG_USER, SEARXNG_PASSWORD.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Andrei Makarov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,201 @@
1
+ # searxng
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/searxng.svg)](https://badge.fury.io/rb/searxng) [![Test Status](https://github.com/amkisko/searxng.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/searxng.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/searxng.rb/graph/badge.svg)](https://codecov.io/gh/amkisko/searxng.rb)
4
+
5
+ Ruby gem providing a SearXNG HTTP client, CLI (search), and MCP (Model Context Protocol) server for web search. Integrates with MCP-compatible clients like Codex, Cursor, Claude, and other MCP-enabled tools.
6
+
7
+ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
8
+
9
+ <a href="https://www.kiskolabs.com">
10
+ <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
11
+ </a>
12
+
13
+ ## Requirements
14
+
15
+ - **Ruby 3.1 or higher** (Ruby 3.0 and earlier are not supported). For managing Ruby versions, [rbenv](https://github.com/rbenv/rbenv) or [mise](https://mise.jdx.dev/) are recommended; system Ruby may be sufficient if it meets the version requirement.
16
+
17
+ ## Quick Start
18
+
19
+ ```bash
20
+ gem install searxng
21
+ ```
22
+
23
+ Or add to your Gemfile:
24
+
25
+ ```ruby
26
+ gem "searxng"
27
+ ```
28
+
29
+ ### Configuration
30
+
31
+ - **SEARXNG_URL** (required for remote): Base URL of your SearXNG instance (e.g. `http://localhost:8080` or `https://search.example.com`). Defaults to `http://localhost:8080` when not set.
32
+ - **SEARXNG_USER** / **SEARXNG_PASSWORD** (optional): Basic auth credentials if your instance is protected.
33
+ - **SEARXNG_CA_FILE** / **SEARXNG_CA_PATH** (optional): Custom CA certificate file or directory for HTTPS. You can also pass `ca_file:`, `ca_path:`, or `verify_mode:` to `Searxng::Client.new`.
34
+ - **SEARXNG_USER_AGENT** (optional): Custom User-Agent string. The client sends a default identifying the gem; if your instance returns 403 Forbidden (e.g. bot detection), set a custom User-Agent or pass `user_agent:` to `Searxng::Client.new`.
35
+
36
+ To fully customize the HTTP client (e.g. custom certs, proxy, timeouts), override `#build_http(uri)` in a subclass, or pass a `configure_http:` callable to the client; it is invoked with the `Net::HTTP` instance and the request URI before each request.
37
+
38
+ ### Cursor IDE Configuration
39
+
40
+ For Cursor IDE, create or update `.cursor/mcp.json` in your project:
41
+
42
+ ```json
43
+ {
44
+ "mcpServers": {
45
+ "searxng": {
46
+ "command": "bundle",
47
+ "args": ["exec", "searxng", "serve"],
48
+ "env": {
49
+ "SEARXNG_URL": "http://localhost:8080"
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ **Note**: Using `gem exec searxng serve` (with `command`: `"gem"`, `args`: `["exec", "searxng", "serve"]`) ensures the correct Ruby version is used when the gem is installed globally.
57
+
58
+ ### Claude Desktop Configuration
59
+
60
+ For Claude Desktop, edit the MCP configuration file:
61
+
62
+ **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
63
+ **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "searxng": {
69
+ "command": "bundle",
70
+ "args": ["exec", "searxng", "serve"],
71
+ "env": {
72
+ "SEARXNG_URL": "http://localhost:8080"
73
+ }
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ **Note**: After updating the configuration, restart Claude Desktop for changes to take effect.
80
+
81
+ ### Running the MCP Server manually
82
+
83
+ After installation, you can start the MCP server immediately:
84
+
85
+ ```bash
86
+ # With bundler
87
+ bundle exec searxng serve
88
+
89
+ # Or if installed globally
90
+ gem exec searxng serve
91
+ ```
92
+
93
+ The server will start and communicate via STDIN/STDOUT using the MCP protocol.
94
+
95
+ ### Testing with MCP Inspector
96
+
97
+ You can test the MCP server using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) tool:
98
+
99
+ ```bash
100
+ # Ensure a SearXNG instance is running (e.g. docker compose -f examples/docker-compose.yml up -d)
101
+ export SEARXNG_URL="http://localhost:8080"
102
+
103
+ # Run the MCP inspector with the server
104
+ npx @modelcontextprotocol/inspector bundle exec searxng serve
105
+ ```
106
+
107
+ The inspector will:
108
+
109
+ 1. Start a proxy server and open a browser interface
110
+ 2. Connect to your MCP server via STDIO
111
+ 3. Allow you to test all available tools interactively
112
+ 4. Display request/response messages and any errors
113
+
114
+ Useful for testing the `searxng_web_search` tool before integrating with Cursor or other MCP clients.
115
+
116
+ ## Features
117
+
118
+ - **SearXNG HTTP Client**: Full-featured client for the SearXNG search API (results, infoboxes, suggestions, answers, corrections)
119
+ - **CLI**: `searxng search` for ad-hoc queries and `searxng serve` for the MCP server
120
+ - **MCP Server Integration**: Ready-to-use MCP server with web search tool, compatible with Cursor IDE, Claude Desktop, and other MCP-enabled tools
121
+ - **Configurable HTTP**: Optional custom CA, verify mode, and `configure_http` hook or `build_http` override for proxy/timeouts/certs
122
+ - **Basic Auth**: Optional Basic authentication via options or ENV
123
+
124
+ ## Basic Usage
125
+
126
+ ### Ruby API
127
+
128
+ ```ruby
129
+ require "searxng"
130
+
131
+ client = Searxng::Client.new
132
+ data = client.search("ruby programming", pageno: 1, language: "en")
133
+
134
+ data[:results].each do |r|
135
+ puts r[:title], r[:url], r[:content]
136
+ end
137
+ ```
138
+
139
+ ### CLI
140
+
141
+ ```bash
142
+ searxng search "your query"
143
+ searxng search "ruby" --page 2 --language en --time-range day --json
144
+ ```
145
+
146
+ Options: `--url`, `--page`, `--language`, `--time-range` (day|month|year), `--safesearch` (0|1|2), `--json`.
147
+
148
+ ## MCP Tools
149
+
150
+ The MCP server provides the following tools:
151
+
152
+ 1. **searxng_web_search** - Web search via SearXNG
153
+ - Parameters: `query` (required), `pageno` (optional), `max_results` (optional, default 10), `time_range` (optional), `language` (optional), `safesearch` (optional). Use `max_results` to limit how many results are returned in one response (reduces token usage); use `pageno` for the next page.
154
+
155
+ ## Examples
156
+
157
+ See [examples/SETUP.md](examples/SETUP.md) for running SearXNG locally (e.g. Docker) and [examples/run_queries.rb](examples/run_queries.rb) for a small script using the client.
158
+
159
+ ## Development
160
+
161
+ ```bash
162
+ # Install dependencies
163
+ bundle install
164
+
165
+ # Run tests
166
+ bundle exec rspec
167
+
168
+ # Run tests across multiple Ruby versions
169
+ bundle exec appraisal install
170
+ bundle exec appraisal rspec
171
+
172
+ # Run linting
173
+ bundle exec rubocop
174
+
175
+ # Validate RBS type signatures
176
+ bundle exec rbs validate
177
+ ```
178
+
179
+ ## Contributing
180
+
181
+ Bug reports and pull requests are welcome on GitHub at https://github.com/amkisko/searxng.rb.
182
+
183
+ Contribution policy:
184
+ - New features are not necessarily added to the gem
185
+ - Pull request should have test coverage for affected parts
186
+ - Pull request should have changelog entry
187
+
188
+ Review policy:
189
+ - It might take up to 2 calendar weeks to review and merge critical fixes
190
+ - It might take up to 6 calendar months to review and merge pull request
191
+ - It might take up to 1 calendar year to review an issue
192
+
193
+ For more information, see [CONTRIBUTING.md](CONTRIBUTING.md).
194
+
195
+ ## Security
196
+
197
+ If you discover a security vulnerability, please report it responsibly. See [SECURITY.md](SECURITY.md) for details.
198
+
199
+ ## License
200
+
201
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/bin/searxng ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ cmd = ARGV.shift
4
+
5
+ case cmd
6
+ when "serve"
7
+ unless ENV["DEBUG"]
8
+ require "stringio"
9
+ $stderr = StringIO.new
10
+ $stderr.set_encoding("UTF-8")
11
+ end
12
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
13
+ require "searxng/server"
14
+ Searxng::Server.start
15
+ when "search"
16
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
17
+ require "searxng"
18
+ require "optparse"
19
+ require "json"
20
+
21
+ base_url = nil
22
+ page = 1
23
+ language = "all"
24
+ time_range = nil
25
+ safesearch = nil
26
+ json_output = false
27
+
28
+ OptionParser.new do |o|
29
+ o.on("--url URL", "SearXNG base URL (overrides SEARXNG_URL)") { |v| base_url = v }
30
+ o.on("--page N", Integer, "Page number (default: 1)") { |v| page = v }
31
+ o.on("--language LANG", "Language code (default: all)") { |v| language = v }
32
+ o.on("--time-range RANGE", "Time range: day, month, year") { |v| time_range = v }
33
+ o.on("--safesearch N", Integer, "Safe search: 0, 1, or 2") { |v| safesearch = v }
34
+ o.on("--json", "Output raw JSON") { json_output = true }
35
+ end.parse!(ARGV)
36
+
37
+ query = ARGV.join(" ").strip
38
+ if query.empty?
39
+ warn "Usage: searxng search QUERY [--url URL] [--page N] [--language LANG] [--time-range day|month|year] [--safesearch 0|1|2] [--json]"
40
+ exit 1
41
+ end
42
+
43
+ opts = { pageno: page, language: language, time_range: time_range, safesearch: safesearch }
44
+ opts[:base_url] = base_url if base_url
45
+
46
+ client = Searxng::Client.new(**opts.slice(:base_url))
47
+ data = client.search(query, **opts.slice(:pageno, :time_range, :language, :safesearch))
48
+
49
+ if json_output
50
+ puts JSON.pretty_generate(data)
51
+ else
52
+ data[:infoboxes]&.each do |ib|
53
+ puts "Infobox: #{ib[:infobox]}"
54
+ puts " #{ib[:content]}"
55
+ puts ""
56
+ end
57
+ data[:results]&.each do |r|
58
+ puts "#{r[:title]}"
59
+ puts " #{r[:url]}"
60
+ puts " #{r[:content]}"
61
+ puts ""
62
+ end
63
+ puts "No results found" if (data[:results].nil? || data[:results].empty?) && (data[:infoboxes].nil? || data[:infoboxes].empty?)
64
+ end
65
+ else
66
+ warn "Usage: searxng (search|serve) ..."
67
+ warn " search QUERY - Run a search and print results"
68
+ warn " serve - Start MCP server (stdio)"
69
+ exit 1
70
+ end
@@ -0,0 +1,142 @@
1
+ require "uri"
2
+ require "net/http"
3
+ require "openssl"
4
+ require "json"
5
+
6
+ module Searxng
7
+ class Client
8
+ DEFAULT_BASE_URL = "http://localhost:8080"
9
+ DEFAULT_USER_AGENT = "searxng-ruby/#{Searxng::VERSION} (https://github.com/amkisko/searxng.rb)".freeze
10
+ VALID_TIME_RANGES = %w[day month year].freeze
11
+ VALID_SAFESEARCH = [0, 1, 2].freeze
12
+
13
+ def initialize(
14
+ base_url: nil,
15
+ user: nil,
16
+ password: nil,
17
+ open_timeout: 10,
18
+ read_timeout: 10,
19
+ ca_file: nil,
20
+ ca_path: nil,
21
+ verify_mode: nil,
22
+ user_agent: nil,
23
+ configure_http: nil
24
+ )
25
+ @base_url = (base_url || ENV["SEARXNG_URL"] || DEFAULT_BASE_URL).to_s.chomp("/")
26
+ @user = user || ENV["SEARXNG_USER"]
27
+ @password = password || ENV["SEARXNG_PASSWORD"]
28
+ @open_timeout = open_timeout
29
+ @read_timeout = read_timeout
30
+ @ca_file = ca_file || ENV["SEARXNG_CA_FILE"]
31
+ @ca_path = ca_path || ENV["SEARXNG_CA_PATH"]
32
+ @verify_mode = verify_mode
33
+ @user_agent = user_agent || ENV["SEARXNG_USER_AGENT"] || DEFAULT_USER_AGENT
34
+ @configure_http = configure_http
35
+ end
36
+
37
+ def search(query, pageno: 1, time_range: nil, language: "all", safesearch: nil)
38
+ raise ConfigurationError, "SEARXNG_URL not set" if @base_url.nil? || @base_url.empty?
39
+
40
+ uri = build_search_uri(query, pageno: pageno, time_range: time_range, language: language, safesearch: safesearch)
41
+ response = perform_request(uri)
42
+ parse_response(response, uri, query)
43
+ end
44
+
45
+ private
46
+
47
+ def build_search_uri(query, pageno:, time_range:, language:, safesearch:)
48
+ uri = URI.join("#{@base_url}/", "search")
49
+ params = {"q" => query, "format" => "json", "pageno" => pageno.to_s}
50
+ params["time_range"] = time_range if time_range && VALID_TIME_RANGES.include?(time_range.to_s)
51
+ params["language"] = language if language && language != "all"
52
+ params["safesearch"] = safesearch.to_s if safesearch && VALID_SAFESEARCH.include?(safesearch.to_i)
53
+ uri.query = URI.encode_www_form(params)
54
+ uri
55
+ end
56
+
57
+ def perform_request(uri)
58
+ http = build_http(uri)
59
+ @configure_http&.call(http, uri)
60
+
61
+ request = Net::HTTP::Get.new(uri)
62
+ request["Accept"] = "application/json"
63
+ request["User-Agent"] = @user_agent
64
+ request.basic_auth(@user, @password) if @user && @password
65
+
66
+ http.request(request)
67
+ rescue SocketError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::ECONNRESET, Errno::EPIPE, Timeout::Error, OpenSSL::SSL::SSLError, EOFError => e
68
+ raise NetworkError, "Connection failed: #{e.message}"
69
+ end
70
+
71
+ # Builds and returns a configured Net::HTTP instance. Override in a subclass to customize
72
+ # timeouts, SSL, proxy, or other options. Called once per request.
73
+ def build_http(uri)
74
+ http = Net::HTTP.new(uri.host, uri.port)
75
+ http.use_ssl = (uri.scheme == "https")
76
+ http.open_timeout = @open_timeout
77
+ http.read_timeout = @read_timeout
78
+ apply_ssl_config(http) if http.use_ssl?
79
+ http
80
+ end
81
+
82
+ def apply_ssl_config(http)
83
+ http.verify_mode = @verify_mode || OpenSSL::SSL::VERIFY_PEER
84
+ default_ca = OpenSSL::X509::DEFAULT_CERT_FILE
85
+ http.ca_file = @ca_file if @ca_file && File.exist?(@ca_file)
86
+ http.ca_file = default_ca if !http.ca_file && File.exist?(default_ca)
87
+ http.ca_path = @ca_path if @ca_path && File.directory?(@ca_path)
88
+ end
89
+
90
+ def parse_response(response, uri, query)
91
+ case response
92
+ when Net::HTTPSuccess
93
+ body = response.body
94
+ data = JSON.parse(body)
95
+ {
96
+ query: data["query"] || query,
97
+ number_of_results: data["number_of_results"],
98
+ results: normalize_results(data["results"] || []),
99
+ infoboxes: normalize_infoboxes(data["infoboxes"] || []),
100
+ suggestions: data["suggestions"] || [],
101
+ answers: data["answers"] || [],
102
+ corrections: data["corrections"] || []
103
+ }
104
+ else
105
+ raise APIError.new(
106
+ "SearXNG returned #{response.code} #{response.message}",
107
+ status_code: response.code.to_i,
108
+ response_data: response.body,
109
+ uri: uri.to_s
110
+ )
111
+ end
112
+ rescue JSON::ParserError => e
113
+ raise APIError.new(
114
+ "Invalid JSON response: #{e.message}",
115
+ response_data: response&.body,
116
+ uri: uri.to_s
117
+ )
118
+ end
119
+
120
+ def normalize_results(results)
121
+ Array(results).map do |r|
122
+ {
123
+ title: r["title"].to_s,
124
+ url: r["url"].to_s,
125
+ content: r["content"].to_s,
126
+ score: (r["score"] || 0).to_f
127
+ }
128
+ end
129
+ end
130
+
131
+ def normalize_infoboxes(infoboxes)
132
+ Array(infoboxes).map do |ib|
133
+ {
134
+ infobox: ib["infobox"].to_s,
135
+ id: ib["id"].to_s,
136
+ content: ib["content"].to_s,
137
+ urls: Array(ib["urls"]).map { |u| {title: u["title"].to_s, url: u["url"].to_s} }
138
+ }
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,18 @@
1
+ module Searxng
2
+ class Error < StandardError; end
3
+
4
+ class ConfigurationError < Error; end
5
+
6
+ class NetworkError < Error; end
7
+
8
+ class APIError < Error
9
+ attr_reader :status_code, :response_data, :uri
10
+
11
+ def initialize(message, status_code: nil, response_data: nil, uri: nil)
12
+ super(message)
13
+ @status_code = status_code
14
+ @response_data = response_data
15
+ @uri = uri
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,131 @@
1
+ require "fast_mcp"
2
+ require "searxng"
3
+
4
+ FastMcp = MCP unless defined?(FastMcp)
5
+
6
+ module Searxng
7
+ class Server
8
+ class NullLogger
9
+ attr_accessor :transport, :client_initialized
10
+
11
+ def initialize
12
+ @transport = nil
13
+ @client_initialized = false
14
+ @level = nil
15
+ end
16
+
17
+ attr_writer :level
18
+ attr_reader :level
19
+
20
+ def debug(*)
21
+ end
22
+
23
+ def info(*)
24
+ end
25
+
26
+ def warn(*)
27
+ end
28
+
29
+ def error(*)
30
+ end
31
+
32
+ def fatal(*)
33
+ end
34
+
35
+ def unknown(*)
36
+ end
37
+
38
+ def client_initialized?
39
+ @client_initialized
40
+ end
41
+
42
+ def set_client_initialized(value = true)
43
+ @client_initialized = value
44
+ end
45
+
46
+ def stdio_transport?
47
+ @transport == :stdio
48
+ end
49
+
50
+ def rack_transport?
51
+ @transport == :rack
52
+ end
53
+ end
54
+
55
+ def self.start
56
+ server = FastMcp::Server.new(
57
+ name: "searxng",
58
+ version: Searxng::VERSION,
59
+ logger: NullLogger.new
60
+ )
61
+ server.register_tool(SearxngWebSearchTool)
62
+ server.start
63
+ end
64
+
65
+ class BaseTool < FastMcp::Tool
66
+ protected
67
+
68
+ def get_client
69
+ Client.new
70
+ end
71
+ end
72
+
73
+ class SearxngWebSearchTool < BaseTool
74
+ tool_name "searxng_web_search"
75
+ description "Performs a web search using the SearXNG API. Use this to find information on the web. Aggregates results from multiple search engines."
76
+
77
+ arguments do
78
+ required(:query).filled(:string).description("The search query")
79
+ optional(:pageno).filled(:integer).description("Search page number (default: 1)")
80
+ optional(:max_results).filled(:integer).description("Max number of results to return in the response (default: 10). Use pageno for more.")
81
+ optional(:time_range).filled(:string).description("Time range: day, month, or year")
82
+ optional(:language).filled(:string).description("Language code (e.g. en, fr). Default: all")
83
+ optional(:safesearch).filled(:integer).description("Safe search: 0=none, 1=moderate, 2=strict")
84
+ end
85
+
86
+ def call(query:, pageno: 1, max_results: 10, time_range: nil, language: "all", safesearch: nil)
87
+ data = get_client.search(
88
+ query,
89
+ pageno: pageno,
90
+ time_range: time_range,
91
+ language: language,
92
+ safesearch: safesearch
93
+ )
94
+ format_search_result(data, max_results: max_results, pageno: pageno)
95
+ end
96
+
97
+ private
98
+
99
+ def format_search_result(data, max_results: 10, pageno: 1)
100
+ out = []
101
+ data[:infoboxes]&.each do |ib|
102
+ out << "Infobox: #{ib[:infobox]}"
103
+ out << "ID: #{ib[:id]}"
104
+ out << "Content: #{ib[:content]}"
105
+ out << ""
106
+ end
107
+ results = data[:results] || []
108
+ total = data[:number_of_results]
109
+ if results.empty?
110
+ out << "No results found"
111
+ else
112
+ limit = [max_results.to_i, 1].max
113
+ shown = results.first(limit)
114
+ shown.each do |r|
115
+ out << "Title: #{r[:title]}"
116
+ out << "URL: #{r[:url]}"
117
+ out << "Content: #{r[:content]}"
118
+ out << "Score: #{r[:score]}"
119
+ out << ""
120
+ end
121
+ if results.size > limit
122
+ out << "Showing #{limit} of #{results.size} on this page. Use pageno=#{pageno + 1} for more."
123
+ elsif total && total > results.size
124
+ out << "Showing #{shown.size} results (page #{pageno}). #{total} total. Use pageno=#{pageno + 1} for more."
125
+ end
126
+ end
127
+ out.join("\n").strip
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,3 @@
1
+ module Searxng
2
+ VERSION = "0.1.0"
3
+ end
data/lib/searxng.rb ADDED
@@ -0,0 +1,6 @@
1
+ require_relative "searxng/version"
2
+ require_relative "searxng/errors"
3
+ require_relative "searxng/client"
4
+
5
+ module Searxng
6
+ end
data/sig/searxng.rbs ADDED
@@ -0,0 +1,36 @@
1
+ module Searxng
2
+ VERSION: String
3
+
4
+ class Error < StandardError; end
5
+ class ConfigurationError < Error; end
6
+ class NetworkError < Error; end
7
+
8
+ class APIError < Error
9
+ def initialize: (String message, ?status_code: Integer?, ?response_data: String?, ?uri: String?) -> void
10
+ attr_reader status_code: Integer?
11
+ attr_reader response_data: String?
12
+ attr_reader uri: String?
13
+ end
14
+
15
+ class Client
16
+ def initialize: (
17
+ ?base_url: String?,
18
+ ?user: String?,
19
+ ?password: String?,
20
+ ?open_timeout: Integer,
21
+ ?read_timeout: Integer,
22
+ ?ca_file: String?,
23
+ ?ca_path: String?,
24
+ ?verify_mode: Integer?,
25
+ ?user_agent: String?,
26
+ ?configure_http: (Net::HTTP, URI::Generic) -> void
27
+ ) -> void
28
+ def search: (
29
+ String query,
30
+ ?pageno: Integer,
31
+ ?time_range: String?,
32
+ ?language: String,
33
+ ?safesearch: Integer?
34
+ ) -> Hash[Symbol, untyped]
35
+ end
36
+ end
metadata ADDED
@@ -0,0 +1,329 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: searxng
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrei Makarov
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: fast-mcp
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '0.1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: rack
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '3.0'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '3.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: base64
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '0.1'
53
+ type: :runtime
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '0.1'
60
+ - !ruby/object:Gem::Dependency
61
+ name: rspec
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '3.13'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '3.13'
74
+ - !ruby/object:Gem::Dependency
75
+ name: webmock
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '3.26'
81
+ type: :development
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '3.26'
88
+ - !ruby/object:Gem::Dependency
89
+ name: vcr
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '6.0'
95
+ type: :development
96
+ prerelease: false
97
+ version_requirements: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '6.0'
102
+ - !ruby/object:Gem::Dependency
103
+ name: rake
104
+ requirement: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '13.3'
109
+ type: :development
110
+ prerelease: false
111
+ version_requirements: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: '13.3'
116
+ - !ruby/object:Gem::Dependency
117
+ name: simplecov
118
+ requirement: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: '0.22'
123
+ type: :development
124
+ prerelease: false
125
+ version_requirements: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: '0.22'
130
+ - !ruby/object:Gem::Dependency
131
+ name: rspec_junit_formatter
132
+ requirement: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - "~>"
135
+ - !ruby/object:Gem::Version
136
+ version: '0.6'
137
+ type: :development
138
+ prerelease: false
139
+ version_requirements: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - "~>"
142
+ - !ruby/object:Gem::Version
143
+ version: '0.6'
144
+ - !ruby/object:Gem::Dependency
145
+ name: simplecov-cobertura
146
+ requirement: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - "~>"
149
+ - !ruby/object:Gem::Version
150
+ version: '3.1'
151
+ type: :development
152
+ prerelease: false
153
+ version_requirements: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - "~>"
156
+ - !ruby/object:Gem::Version
157
+ version: '3.1'
158
+ - !ruby/object:Gem::Dependency
159
+ name: standard
160
+ requirement: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - "~>"
163
+ - !ruby/object:Gem::Version
164
+ version: '1.52'
165
+ type: :development
166
+ prerelease: false
167
+ version_requirements: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - "~>"
170
+ - !ruby/object:Gem::Version
171
+ version: '1.52'
172
+ - !ruby/object:Gem::Dependency
173
+ name: standard-custom
174
+ requirement: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - "~>"
177
+ - !ruby/object:Gem::Version
178
+ version: '1.0'
179
+ type: :development
180
+ prerelease: false
181
+ version_requirements: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - "~>"
184
+ - !ruby/object:Gem::Version
185
+ version: '1.0'
186
+ - !ruby/object:Gem::Dependency
187
+ name: standard-performance
188
+ requirement: !ruby/object:Gem::Requirement
189
+ requirements:
190
+ - - "~>"
191
+ - !ruby/object:Gem::Version
192
+ version: '1.8'
193
+ type: :development
194
+ prerelease: false
195
+ version_requirements: !ruby/object:Gem::Requirement
196
+ requirements:
197
+ - - "~>"
198
+ - !ruby/object:Gem::Version
199
+ version: '1.8'
200
+ - !ruby/object:Gem::Dependency
201
+ name: standard-rspec
202
+ requirement: !ruby/object:Gem::Requirement
203
+ requirements:
204
+ - - "~>"
205
+ - !ruby/object:Gem::Version
206
+ version: '0.3'
207
+ type: :development
208
+ prerelease: false
209
+ version_requirements: !ruby/object:Gem::Requirement
210
+ requirements:
211
+ - - "~>"
212
+ - !ruby/object:Gem::Version
213
+ version: '0.3'
214
+ - !ruby/object:Gem::Dependency
215
+ name: rubocop-rspec
216
+ requirement: !ruby/object:Gem::Requirement
217
+ requirements:
218
+ - - "~>"
219
+ - !ruby/object:Gem::Version
220
+ version: '3.8'
221
+ type: :development
222
+ prerelease: false
223
+ version_requirements: !ruby/object:Gem::Requirement
224
+ requirements:
225
+ - - "~>"
226
+ - !ruby/object:Gem::Version
227
+ version: '3.8'
228
+ - !ruby/object:Gem::Dependency
229
+ name: rubocop-thread_safety
230
+ requirement: !ruby/object:Gem::Requirement
231
+ requirements:
232
+ - - "~>"
233
+ - !ruby/object:Gem::Version
234
+ version: '0.7'
235
+ type: :development
236
+ prerelease: false
237
+ version_requirements: !ruby/object:Gem::Requirement
238
+ requirements:
239
+ - - "~>"
240
+ - !ruby/object:Gem::Version
241
+ version: '0.7'
242
+ - !ruby/object:Gem::Dependency
243
+ name: appraisal
244
+ requirement: !ruby/object:Gem::Requirement
245
+ requirements:
246
+ - - "~>"
247
+ - !ruby/object:Gem::Version
248
+ version: '2.5'
249
+ type: :development
250
+ prerelease: false
251
+ version_requirements: !ruby/object:Gem::Requirement
252
+ requirements:
253
+ - - "~>"
254
+ - !ruby/object:Gem::Version
255
+ version: '2.5'
256
+ - !ruby/object:Gem::Dependency
257
+ name: memory_profiler
258
+ requirement: !ruby/object:Gem::Requirement
259
+ requirements:
260
+ - - "~>"
261
+ - !ruby/object:Gem::Version
262
+ version: '1.1'
263
+ type: :development
264
+ prerelease: false
265
+ version_requirements: !ruby/object:Gem::Requirement
266
+ requirements:
267
+ - - "~>"
268
+ - !ruby/object:Gem::Version
269
+ version: '1.1'
270
+ - !ruby/object:Gem::Dependency
271
+ name: rbs
272
+ requirement: !ruby/object:Gem::Requirement
273
+ requirements:
274
+ - - "~>"
275
+ - !ruby/object:Gem::Version
276
+ version: '3.9'
277
+ type: :development
278
+ prerelease: false
279
+ version_requirements: !ruby/object:Gem::Requirement
280
+ requirements:
281
+ - - "~>"
282
+ - !ruby/object:Gem::Version
283
+ version: '3.9'
284
+ description: Ruby gem providing a SearXNG HTTP client, CLI (search), and MCP server
285
+ for web search. Integrates with Cursor IDE via Model Context Protocol.
286
+ email:
287
+ - andrei@kiskolabs.com
288
+ executables:
289
+ - searxng
290
+ extensions: []
291
+ extra_rdoc_files: []
292
+ files:
293
+ - CHANGELOG.md
294
+ - LICENSE
295
+ - README.md
296
+ - bin/searxng
297
+ - lib/searxng.rb
298
+ - lib/searxng/client.rb
299
+ - lib/searxng/errors.rb
300
+ - lib/searxng/server.rb
301
+ - lib/searxng/version.rb
302
+ - sig/searxng.rbs
303
+ homepage: https://github.com/amkisko/searxng.rb
304
+ licenses:
305
+ - MIT
306
+ metadata:
307
+ source_code_uri: https://github.com/amkisko/searxng.rb
308
+ changelog_uri: https://github.com/amkisko/searxng.rb/blob/main/CHANGELOG.md
309
+ bug_tracker_uri: https://github.com/amkisko/searxng.rb/issues
310
+ documentation_uri: https://github.com/amkisko/searxng.rb#readme
311
+ rubygems_mfa_required: 'true'
312
+ rdoc_options: []
313
+ require_paths:
314
+ - lib
315
+ required_ruby_version: !ruby/object:Gem::Requirement
316
+ requirements:
317
+ - - ">="
318
+ - !ruby/object:Gem::Version
319
+ version: '3.1'
320
+ required_rubygems_version: !ruby/object:Gem::Requirement
321
+ requirements:
322
+ - - ">="
323
+ - !ruby/object:Gem::Version
324
+ version: '0'
325
+ requirements: []
326
+ rubygems_version: 4.0.3
327
+ specification_version: 4
328
+ summary: SearXNG Ruby client and MCP server
329
+ test_files: []