All environment variables for mcp-searxng, organized by concern. All variables are optional unless marked required.
| Variable | Required | Default | Description |
|---|---|---|---|
SEARXNG_URL |
Yes | — | URL of your SearXNG instance, or a semicolon-separated list of interchangeable replica base URLs. Single URL behavior is unchanged. Format: <protocol>://[username[:password]@]<hostname>[:<port>][/path] (e.g. http://localhost:8080, https://user:pass@search.example.com, https://searx.example.com/searxng, or https://user:pass@one.example.com;https://two.example.com) |
SEARXNG_FANOUT |
No | false |
Set to true to query all healthy configured SearXNG instances in parallel and merge results. Default failover mode tries instances in order until one returns results. |
When SEARXNG_URL contains multiple semicolon-separated URLs, they are treated as interchangeable replicas. Default mode fails over in order when an instance hard-fails or returns no results. A reachable 200 OK response with an empty results array is considered healthy and does not trigger cooldown. Instances with 3 consecutive hard failures are skipped for 60 seconds.
With SEARXNG_FANOUT=true, all healthy instances are queried in parallel. Results are deduplicated by canonical URL, the copy with the highest score is kept, and merged results are ordered by descending score. Capability discovery and filter guidance aggregate /config data from all reachable configured instances; common categories/engines work everywhere reachable, while available values are best-effort. A /config endpoint that fails is skipped for about 60 seconds before retry, or retried immediately when searxng_instance_info is called with refresh=true. Search suggestions use the first configured instance.
For SearXNG setup, direct verification, and replica troubleshooting, see Operating Self-Hosted SearXNG with mcp-searxng. For MCP-process capacity planning and optional Docker limits, see the measured deployment profiles.
For SearXNG instances protected with HTTP Basic Auth, embed credentials in each SEARXNG_URL entry:
SEARXNG_URL=https://username:password@search.example.comFor multiple interchangeable replicas, each semicolon-separated URL can carry its own credentials. This supports mixed deployments such as one private auth-gated instance and one public instance without sending the private credentials to the public host:
SEARXNG_URL=https://alice:secret@private-search.example.com;https://public-search.example.comPercent-encode special characters in usernames or passwords before placing them in the URL. For example, password p@ss should be written as p%40ss.
| Variable | Required | Default | Description |
|---|---|---|---|
AUTH_USERNAME |
No | — | Legacy global HTTP Basic Auth username fallback used only when a SEARXNG_URL entry has no userinfo |
AUTH_PASSWORD |
No | — | Legacy global HTTP Basic Auth password fallback used only when a SEARXNG_URL entry has no userinfo |
| Variable | Required | Default | Description |
|---|---|---|---|
SEARXNG_TIMEOUT_MS |
No | 10000 |
Maximum time in milliseconds for each SearXNG search attempt, covering response headers, body streaming, decoding, and parsing. The request is aborted and a network error is returned if the server does not complete within this window. Invalid, non-positive, or out-of-range values (above 2147483647) fall back to the default. |
FETCH_TIMEOUT_MS |
No | 10000 |
Maximum time in milliseconds to wait for a web_url_read fetch. The request is aborted and an error is returned if the server does not respond within this window. |
SEARXNG_MAX_RESPONSE_BYTES |
No | 5242880 |
Maximum retained bytes read from each SearXNG response: successful search JSON, HTML fallback, /config, and /autocompleter. It is a per-response admission limit, not an aggregate or concurrency cap. |
SEARXNG_MAX_RESPONSE_BYTES accepts only a strict integer after surrounding JavaScript whitespace is trimmed. A leading + and leading zeros are accepted; -0, other non-positive values, fractions, exponents, suffixes, unsafe integers, and values outside the inclusive 1 through 16777216 range are invalid. Unset or blank uses 5242880 silently. An invalid value uses that default and emits one value-free warning per MCP server.
Search JSON and HTML fallback each receive their own full SEARXNG_TIMEOUT_MS deadline, so the fallback deadline is separate and additive; replica failover remains per instance and additive as well. /config and /autocompleter retain their fixed 5-second deadlines through body consumption. Oversized, stalled, partial, malformed, or read-failed responses do not populate successful caches or health state and follow the existing failure, negative-cache, or empty-array path.
| Variable | Required | Default | Description |
|---|---|---|---|
SEARXNG_LITE_TOOLS |
No | false |
Set to true to register minimal tool schemas with only query / url parameters. Reduces per-call token overhead for local models with small context windows. Extra parameters (e.g. language, maxLength) passed by the caller are still accepted and forwarded. |
Operator-level defaults applied when the caller omits the corresponding per-call parameter.
| Variable | Required | Default | Description |
|---|---|---|---|
SEARXNG_DEFAULT_LANGUAGE |
No | all |
Default language for all searches when language is not passed per call (e.g. en, fr, de). |
SEARXNG_DEFAULT_SAFESEARCH |
No | — | Default safe-search level: 0 (off), 1 (moderate), 2 (strict). Invalid values are ignored with a warning. When unset, the SearXNG instance default applies. |
SEARXNG_DEFAULT_RESPONSE_FORMAT |
No | text |
After trimming whitespace, accepted values are the exact lowercase text or json. Unset or blank values use text silently; invalid values warn once per server instance and use text. If omitted, SEARXNG_DEFAULT_RESPONSE_FORMAT applies; if unset or invalid, text is used. An explicit response_format always takes precedence. |
Clients that explicitly send or auto-inject response_format=text continue to override the operator default. If an omitted call still returns text after configuring JSON, inspect the tool arguments emitted by the MCP client.
The response-format default also applies when SEARXNG_LITE_TOOLS=true. Lite schemas omit optional parameters, but callers that send response_format explicitly still override the configured default, consistent with the existing forwarding behavior for extra lite-tool arguments.
| Variable | Required | Default | Description |
|---|---|---|---|
SEARXNG_MAX_RESULTS |
No | — | Operator-level maximum number of search results to return per call (1-20). Invalid values are ignored. Recommended: 10 for smaller context windows. |
SEARXNG_MAX_RESULT_CHARS |
No | — | Maximum characters to include in each search result snippet in both compact and full text/JSON responses, including full JSON for existing users who already set this variable. Longer snippets are truncated and marked with …; titles, URLs, and other metadata are never truncated. Compact text normalizes line separators before applying the cap, while JSON applies the cap to the original string value. Invalid values are ignored. Recommended: 500 for smaller context windows. |
SEARCH_CACHE_TTL_MS |
No | 86400000 |
Search result cache TTL in milliseconds. Invalid or non-positive values fall back to the default (24 hours). |
SEARCH_CACHE_MAX_ENTRIES |
No | 200 |
Maximum number of cached search queries. When the cache exceeds this size, the least frequently used entry is evicted, with oldest entry used as the tie-breaker. Invalid or non-positive values fall back to the default. |
Search results are cached in memory per process only; cache contents are not persisted across restarts. Cached text responses are marked with _Cached result_. Cached JSON responses remain parseable and include a top-level "cached": true field.
Self-hosting SearXNG with JSON output enabled remains the recommended setup. The HTML fallback is best-effort for public instances that reject format=json; HTML theme differences may limit parsed metadata.
For the self-hosted SearXNG configuration and verification procedure, see Operating Self-Hosted SearXNG with mcp-searxng. For an instance you do not control, see Using a Public SearXNG Instance with mcp-searxng for trust, evaluation, and conservative-use guidance.
| Variable | Required | Default | Description |
|---|---|---|---|
SEARXNG_HTML_FALLBACK |
No | false |
Set to true to retry 403/404 or non-JSON search responses as an HTML search page and parse title, URL, and snippet only. HTML fallback results are marked with sourceFormat: "html" in JSON output. |
| Variable | Required | Default | Description |
|---|---|---|---|
URL_READ_MAX_CHARS |
No | — | Default maximum characters returned by web_url_read when the caller omits maxLength. Explicit maxLength always wins. Invalid values are ignored. |
URL_READ_MAX_CONTENT_LENGTH_BYTES |
No | 5242880 |
Maximum decompressed response-body bytes web_url_read will read while streaming a page. A HEAD Content-Length preflight may reject oversized pages before GET, but the streaming cap is authoritative. PDF input and extracted text additionally have a fixed 16 MiB ceiling. Invalid values fall back to the default. |
FLARESOLVERR_URL |
No | — | Base URL of a trusted FlareSolverr service, such as http://flaresolverr:8191. When set, web_url_read attempts to ask its /v1 API for a browser session after an uncached URL passes URL validation and the HEAD size preflight. |
FLARESOLVERR_TIMEOUT_MS |
No | 60000 |
Maximum session-acquisition time in milliseconds, from 1 through 300000. Invalid values use the default. This is separate from FETCH_TIMEOUT_MS, which starts when the target is replayed. |
FLARESOLVERR_MAX_CONCURRENT_REQUESTS |
No | 2 |
Maximum concurrent FlareSolverr acquisitions per MCP process, from 1 through 16. In dual mode, a full primary limit advances to Byparr instead of waiting in a queue. |
BYPARR_URL |
No | — | Base URL of a trusted Byparr service, such as http://byparr:8191. It may be configured alone or with FLARESOLVERR_URL; dual mode always tries FlareSolverr first. |
BYPARR_TIMEOUT_SECONDS |
No | 60 |
Maximum Byparr session-acquisition time in whole seconds, from 1 through 300. Invalid values use the default. |
BYPARR_MAX_CONCURRENT_REQUESTS |
No | 2 |
Maximum concurrent Byparr acquisitions per MCP process, from 1 through 16. It is independent from the FlareSolverr counter. |
CACHE_TTL_MS |
No | 86400000 |
URL cache TTL in milliseconds. Invalid or non-positive values fall back to the default (24 hours). |
CACHE_MAX_ENTRIES |
No | 500 |
Maximum number of cached URLs. When the cache exceeds this size, the least frequently used entry is evicted, with oldest entry used as the tie-breaker. Invalid or non-positive values fall back to the default. |
FlareSolverr 3.5.0 and Byparr 2.1.0 were verified on 2026-07-30. Configure either provider, both providers, or neither provider. With both endpoints, FlareSolverr is always primary and Byparr is the secondary; ordering is not configurable and automatic reverse failover is not performed. Canonically identical endpoints fail startup without echoing either configured value.
The verified linux/amd64 images came from multi-architecture manifests
ghcr.io/flaresolverr/flaresolverr:v3.5.0@sha256:139dfee1c6f89249c8d665d1333a42e8ec74ec0a86bc6bb1c8461e10d3a66a47
and
ghcr.io/thephaseless/byparr:2.1.0@sha256:01a46a2865d9a6db5eb8ead04ec0dd33b8fbe233e8565ae70b50d4cc0af4cfb0.
Client cancellation stops local work promptly, but a remote browser may
continue until its configured provider timeout after the HTTP client
disconnects. See browser solver verification.
Each provider URL accepts either an absolute HTTP(S) service base URL or an
already-complete /v1 endpoint; the suffix is normalized idempotently. Query
strings, fragments, userinfo, and other URL schemes are rejected at startup and
again during request resolution without echoing the configured value.
This is stricter than the previous per-read validation: an existing
FLARESOLVERR_URL containing userinfo, a query, a fragment, or an invalid
scheme now prevents startup until corrected.
FLARESOLVERR_TIMEOUT_MS is sent to the solver as its browser-work budget;
BYPARR_TIMEOUT_SECONDS is sent in seconds. The client permits up to 5
additional seconds to receive and validate either solver response.
With at least one browser-solver endpoint configured, web_url_read first performs its normal
target URL security and HEAD size preflight for every uncached read. It then
requests a browser session and uses only its cookies and user-agent. Byparr
2.1.0 also returns rendered content; that field is discarded after a bounded
parse.
When a solver slot is available, every uncached URL that passes URL validation
and the HEAD size preflight is disclosed to that provider. In dual mode the
same original URL can therefore be disclosed first to FlareSolverr and then to
Byparr after an allowed primary failure. The providers have independent
concurrency counters. Cache hits bypass acquisition. The actual target is
fetched by mcp-searxng, so
redirect validation, URL-reader proxy selection, streaming size limits, and
content-type handling remain authoritative.
Replay starts again at the originally requested URL rather than trusting a
same-host path returned by the solver.
A transient solver connection, timeout, overload, HTTP 408/429/5xx response,
malformed response, or oversized response advances to the next configured
provider. If the final provider is busy or unavailable, one uncached direct
URL-reader fetch runs. Invalid solver configuration, other HTTP 4xx responses, a
solver result for a different hostname, and a non-success target status
reported by the solver fail closed. A direct-fetch fallback result is not
cached, so repeated reads re-fetch until solver acquisition succeeds.
Solver-backed cache entries are isolated by provider and from direct-fetch
entries. Cancellation never falls back or writes a cache entry. When the
replay response is application/pdf, the URL reader applies its bounded PDF
text-extraction path.
There is no shared solver timeout. At defaults, the additive maximum is 150
seconds: up to 10 seconds for the initial HEAD preflight, 65 seconds for each
provider including response grace, and 10 seconds for the final direct GET.
MCP cancellation stops the chain immediately when the client propagates it.
Repeated value-free unavailable warnings for one provider should be monitored
as persistent degradation.
Example with the official FlareSolverr image:
services:
mcp-searxng:
image: isokoliuk/mcp-searxng:latest
stdin_open: true
environment:
- SEARXNG_URL=${SEARXNG_URL:?Set SEARXNG_URL in the environment}
- FLARESOLVERR_URL=http://flaresolverr:8191
depends_on:
- flaresolverr
flaresolverr:
image: flaresolverr/flaresolverr:v3.5.0
environment:
- LOG_LEVEL=info
- LOG_HTML=false
- CAPTCHA_SOLVER=${CAPTCHA_SOLVER:-none}
- TZ=America/ChicagoEquivalent Byparr configuration (use this block instead of FlareSolverr):
services:
mcp-searxng:
image: isokoliuk/mcp-searxng:latest
stdin_open: true
environment:
- SEARXNG_URL=${SEARXNG_URL:?Set SEARXNG_URL in the environment}
- BYPARR_URL=http://byparr:8191
- BYPARR_TIMEOUT_SECONDS=60
- BYPARR_MAX_CONCURRENT_REQUESTS=2
depends_on:
- byparr
byparr:
image: ghcr.io/thephaseless/byparr:2.1.0For dual-provider mode, combine both services and set both endpoint variables:
services:
mcp-searxng:
image: isokoliuk/mcp-searxng:latest
stdin_open: true
environment:
- SEARXNG_URL=${SEARXNG_URL:?Set SEARXNG_URL in the environment}
- FLARESOLVERR_URL=http://flaresolverr:8191
- BYPARR_URL=http://byparr:8191
depends_on:
- flaresolverr
- byparr
flaresolverr:
image: flaresolverr/flaresolverr:v3.5.0
environment:
- LOG_LEVEL=info
- LOG_HTML=false
- CAPTCHA_SOLVER=${CAPTCHA_SOLVER:-none}
byparr:
image: ghcr.io/thephaseless/byparr:2.1.0The solver is an operator-trusted browser service. Keep it on a private container network, do not expose port 8191 publicly, and restrict its egress from private services and cloud metadata endpoints. See SECURITY.md for the trust boundary.
| Variable | Required | Default | Description |
|---|---|---|---|
USER_AGENT |
No | — | Global default User-Agent header for outgoing requests (e.g. MyBot/1.0) |
SEARCH_USER_AGENT |
No | USER_AGENT |
User-Agent for SearXNG instance requests: searxng_web_search, /config capability discovery, and search suggestions |
URL_READER_USER_AGENT |
No | USER_AGENT |
User-Agent for web_url_read only |
SEARCH_USER_AGENT and URL_READER_USER_AGENT are per-group overrides. When unset, both fall back to USER_AGENT. If neither the group override nor USER_AGENT is set, no User-Agent header is added by mcp-searxng.
When a browser solver returns a solved session, its browser User-Agent replaces
URL_READER_USER_AGENT / USER_AGENT on the replay fetch because the returned
cookies are tied to that browser identity. The configured URL-reader User-Agent
still applies to the pre-solve HEAD size check and to direct or fallback reads.
Interface-specific proxies take priority over global proxies for their respective tools.
| Variable | Required | Default | Description |
|---|---|---|---|
HTTP_PROXY / HTTPS_PROXY |
No | — | Global proxy for all traffic. Format: http://[user:pass@]host:port |
SEARCH_HTTP_PROXY / SEARCH_HTTPS_PROXY |
No | — | Proxy for all SearXNG-bound traffic: search, suggestions, and capability discovery |
URL_READER_HTTP_PROXY / URL_READER_HTTPS_PROXY |
No | — | Proxy for web_url_read only |
NO_PROXY |
No | — | Comma-separated bypass list (e.g. localhost,.internal,example.com) |
The solver API request uses only the global HTTP_PROXY / HTTPS_PROXY and
NO_PROXY settings because the selected endpoint identifies an operator-trusted
service. The target replay continues to use the URL-reader-specific proxy
settings first.
Proxy variables route traffic through a proxy. Corporate TLS inspection is a separate trust problem: the proxy re-signs upstream certificates, so Node.js must trust the proxy's root CA.
On Linux and macOS, mcp-searxng auto-detects the first readable system CA bundle from these paths:
/etc/ssl/certs/ca-certificates.crt— Debian/Ubuntu/WSL2/etc/pki/tls/certs/ca-bundle.crt— RHEL/CentOS/Fedora/etc/ssl/ca-bundle.pem— OpenSUSE/etc/ssl/cert.pem— Alpine, macOS
If your deployment needs an additional corporate CA, set the standard Node.js NODE_EXTRA_CA_CERTS environment variable to a PEM file. This is a Node.js TLS setting, not an mcp-searxng configuration variable.
Windows has no universal CA bundle file path, so system CA auto-detection is skipped. If you are behind a TLS-inspecting corporate proxy (for example Zscaler, Netskope, Palo Alto, or Blue Coat) and see errors such as UNABLE_TO_GET_ISSUER_CERT_LOCALLY or self signed certificate in certificate chain, export the proxy root CA to PEM and point NODE_EXTRA_CA_CERTS at it.
# Export from Windows cert store (adjust the subject match to your proxy CA):
$cert = Get-ChildItem Cert:\LocalMachine\Root | Where-Object { $_.Subject -match "YourCorp" } | Select-Object -First 1
[System.IO.File]::WriteAllBytes("$env:USERPROFILE\corp-ca.cer", $cert.RawData)
certutil -encode "$env:USERPROFILE\corp-ca.cer" "$env:USERPROFILE\corp-ca.pem"Example MCP client environment block:
{
"env": {
"SEARXNG_URL": "https://searxng.example.com",
"NODE_EXTRA_CA_CERTS": "C:\\Users\\you\\corp-ca.pem"
}
}Never set NODE_TLS_REJECT_UNAUTHORIZED=0. It disables all TLS certificate validation for the Node.js process and makes HTTPS connections vulnerable to interception.
By default the server communicates over STDIO. Set MCP_HTTP_PORT to enable HTTP mode instead. The SDK v2 server accepts modern MCP requests and retains legacy compatibility; modern HTTP clients should send the negotiated MCP-Protocol-Version header.
Both transports support modern 2026-07-28 plus legacy 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, and 2024-10-07. Modern HTTP requests are sessionless POSTs; legacy HTTP requests retain the stateful default or the configured legacy stateless mode.
Modern HTTP requests always use an isolated per-request server and are bounded by MCP_HTTP_STATELESS_MAX_IN_FLIGHT, MCP_HTTP_STATELESS_MAX_IN_FLIGHT_PER_IP, and MCP_HTTP_STATELESS_REQUEST_TIMEOUT_MS, even when MCP_HTTP_STATELESS is false. Setting MCP_HTTP_STATELESS=true extends that per-request serving model and the shared capacity controls to retained legacy POST requests.
The published server SDK 2.0.0 has a temporary compatibility guard for a 2026-07-28 request that omits that header: it returns the standard HTTP 400 HeaderMismatch response. The guard will be removed only after upgrading to a stable SDK containing upstream PR 2594 and proving that the SDK itself returns the same response.
| Variable | Required | Default | Description |
|---|---|---|---|
MCP_HTTP_PORT |
No | — | Port number to enable HTTP transport (e.g. 3000) |
MCP_HTTP_HOST |
No | 127.0.0.1 |
Interface address to bind to. Defaults to localhost-only for security. Set 0.0.0.0 for all interfaces (required for Docker and remote deployments), or a specific IP. Works in pair with MCP_HTTP_PORT only. Breaking change from v1.2.1: previous default was 0.0.0.0. |
MCP_HTTP_TRUST_PROXY |
No | false |
Express trust proxy setting for deployments behind a trusted reverse proxy. Use true, a trusted hop count such as 1, or a proxy subnet/preset such as loopback or 10.0.0.0/8. Unset, false, or 0 disables it (the secure default). |
MCP_HTTP_STATELESS |
No | false |
Set to the exact value true to create an isolated MCP server and transport for every retained legacy POST /mcp; modern POSTs are always isolated. false, blank, or unset retains legacy stateful mode; any other nonblank value warns and uses false. Intended for deployments that cannot preserve process-local legacy sessions. |
MCP_HTTP_STATELESS_MAX_IN_FLIGHT |
No | 16 (range 1-256) |
Global maximum number of admitted modern or legacy-stateless POST requests in flight. Invalid values use the default. |
MCP_HTTP_STATELESS_MAX_IN_FLIGHT_PER_IP |
No | 8 (range 1-global cap) |
Per-client-IP in-flight maximum for modern or legacy-stateless POSTs. Values above the normalized global cap are reduced to that cap. |
MCP_HTTP_STATELESS_REQUEST_TIMEOUT_MS |
No | 900000 (range 1000-2147483647) |
Maximum lifetime of an admitted modern or legacy-stateless POST, including server construction, MCP handling, and an active response stream. |
HTTP endpoints (when HTTP mode is active):
- Modern:
POST /mcp— sessionless MCP protocol - Legacy stateful default:
POST/GET/DELETE /mcp— session-based MCP protocol - With
MCP_HTTP_STATELESS=true:POST /mcponly; GET and DELETE return HTTP 405 withAllow: POST GET /health— health check
HTTP sessions are stored in memory per process. A stale or unknown mcp-session-id on a non-initialize POST /mcp receives HTTP 404 with JSON-RPC error code -32001 and message "Session not found". Clients should recover by running initialize again; initialize requests are accepted even when they still carry a stale session header.
In stateless mode, every POST creates a fresh MCP server and transport, ignores incoming mcp-session-id headers, and never emits a response session ID. A POST can return negotiated JSON or an SSE stream within that same POST. Cross-request sessions, resumable streams, standalone GET notification streams, and DELETE-based session termination are unavailable. Modern requests use the SDK v2 request handler; retained legacy requests use the Node transport.
Rate limiting is always active in HTTP mode to prevent resource exhaustion. Before the MCP handler runs, each request is counted by resolved client IP against exactly one limit. In stateful mode, retained legacy POST requests with a currently live session use the session limit; modern sessionless POST requests and all other POST requests use the initialization limit, even if they present a live legacy mcp-session-id. GET/DELETE requests always use the session limit. In stateless mode, only a single parsed request object recognized by the SDK as initialize uses the initialization limit; all other POST bodies, including notifications and batches, use the session limit, and GET/DELETE still use the session limit. Malformed or oversized JSON is rejected by parsing before rate limiting or MCP server construction.
Each MCP_RATE_* value must be a positive decimal safe integer after JavaScript whitespace trimming. A leading + and leading zeros are accepted; fractions, suffixes, exponents, hexadecimal forms, non-positive values, and integers above Number.MAX_SAFE_INTEGER are rejected. An invalid value uses the documented default and emits one startup warning per variable without copying the raw value into diagnostics. Blank or unset variables use the default silently.
Before this correction, spellings such as 20requests, 12.5, or 1e3 could be accepted as numeric prefixes. They now fall back to the documented default, which may be looser or stricter than the value an older process effectively used. Check startup warnings and correct the environment value rather than relying on the fallback.
| Variable | Required | Default | Description |
|---|---|---|---|
MCP_RATE_WINDOW_MS |
No | 60000 |
Sliding window duration in milliseconds for all rate limits |
MCP_RATE_INIT_MAX |
No | 20 |
Max POST /mcp requests for modern sessionless traffic and requests without a currently live retained legacy session in stateful mode, plus SDK-recognized initialize requests in stateless mode. Guards initialization, invalid, unknown-session, stale-session, and legacy-session-header borrowing. |
MCP_RATE_SESSION_MAX |
No | 300 |
Max POST /mcp requests for currently live retained legacy sessions and all GET/DELETE /mcp requests per window, including GET/DELETE requests with missing or invalid session IDs. Intentionally generous for AI agents. |
Requests exceeding a limit receive HTTP 429 with a JSON-RPC error body (code: -32029). /health has a fixed limit of 60 requests per minute. Standard RateLimit-* headers are included on all responses.
After a stateless request consumes its rate-limit token and passes authorization plus hardened Host/Origin checks, it must also acquire the per-IP and global in-flight capacity slots. Per-IP capacity is checked first. Saturation returns HTTP 503, Retry-After: 1, and JSON-RPC code -32000 with message Server busy; these attempts still consume their selected rate-limit token. A request that exceeds its lifetime before response headers receives HTTP 504 and JSON-RPC code -32000 with message Stateless request timed out. If an SSE response has already started, the connection is closed instead because its status can no longer be changed. Resource cleanup is bounded and capacity is reclaimed after completion, disconnect, failure, or timeout.
The in-memory store is per-process; for horizontally scaled deployments replace it with a shared Redis store via express-rate-limit's store option.
Tool invocation admission is a separate, process-local protection for MCP
tools/call work. It applies uniformly to STDIO, stateful HTTP sessions, and
stateless HTTP requests; it does not replace or change the HTTP request limiter
above. One busy session or transport can consume the shared process budget.
| Variable | Required | Default | Accepted range | Description |
|---|---|---|---|---|
MCP_TOOL_RATE_WINDOW_MS |
No | 60000 |
1000-2147483647 |
Fixed admission window in milliseconds. |
MCP_TOOL_RATE_MAX |
No | 300 |
1-10000 |
Tool-call attempts admitted per fixed window. |
MCP_TOOL_MAX_IN_FLIGHT |
No | 16 |
1-256 |
Hard no-queue ceiling for accepted tool calls executing at once. |
Values use the same strict safe-integer grammar as the HTTP rate controls:
surrounding whitespace, a leading + or -, and leading zeros are accepted,
but suffixes, fractions, exponents, unsafe integers, and out-of-range values
are rejected. Blank or unset values use their defaults silently. Each invalid
nonblank value falls back to its default and emits one startup-only stderr
warning naming the variable and fallback, never the supplied value. Values are
read once when the process starts.
MCP_TOOL_RATE_WINDOW_MS=60000
MCP_TOOL_RATE_MAX=300
MCP_TOOL_MAX_IN_FLIGHT=16The window starts lazily with the first tool attempt and uses a monotonic clock. It is an ordinary fixed window, so calls immediately on both sides of a window boundary can produce a burst of up to twice the configured maximum. Every tool attempt consumes a rate token, including malformed, unknown, and concurrency- rejected calls. After rate admission, execution must acquire an in-flight slot; there is no queue. A rejected call receives the stable retry-with-backoff tool error without configured limits, counters, timing details, arguments, or other call data.
Size the limits for expected workload and have clients back off on rejection. Use separate process replicas when stronger tenant isolation is required. A never-settling handler intentionally retains its in-flight slot until process restart; this policy does not add a watchdog or alter existing cancellation and timeout timing.
When HTTP mode runs behind a trusted reverse proxy, set MCP_HTTP_TRUST_PROXY so Express can resolve the client IP from proxy headers before rate-limit keys and request logs are computed. For a single trusted proxy hop, use MCP_HTTP_TRUST_PROXY=1. Leave it unset for direct exposure; enabling it without a trusted proxy lets clients spoof X-Forwarded-For. This setting is distinct from outbound HTTP_PROXY / HTTPS_PROXY, which control this server's requests to SearXNG or URLs.
Requests whose client IP cannot be resolved share one fail-closed capacity bucket. This prevents missing identity data from bypassing the per-IP cap, but such requests can receive HTTP 503 when another unresolved-IP request occupies that bucket.
Opt-in security layer for when you expose the HTTP transport on a network. Default HTTP behavior is unchanged — hardening must be explicitly enabled with MCP_HTTP_HARDEN=true.
| Variable | Required | Default | Description |
|---|---|---|---|
MCP_HTTP_HARDEN |
No | false |
Set to true to enable all hardening features |
MCP_HTTP_AUTH_TOKEN |
No | — | Required bearer token for all HTTP requests in hardened mode |
MCP_HTTP_ALLOWED_ORIGINS |
No | — | Comma-separated CORS origin allowlist (e.g. https://app.example.com) |
MCP_HTTP_ALLOWED_HOSTS |
No | 127.0.0.1, localhost, [::1] (+ their :PORT forms) |
Comma-separated DNS-rebinding allowlist. Entries are matched exactly against the request Host header, including the port (e.g. app.example.com:8443). Setting this replaces the default entirely. |
MCP_HTTP_ALLOW_PRIVATE_URLS |
No | false |
Allow web_url_read to fetch internal/private URLs, including hostnames that DNS-resolve to private/internal addresses. Private URL reads are blocked by default in all modes. |
MCP_HTTP_EXPOSE_FULL_CONFIG |
No | false |
In hardened mode, include configured SEARXNG_URL value(s), with URL credentials redacted, in the config://server-config MCP resource when this flag is true; when false, report only whether a URL is configured. Non-hardened mode always includes the redacted URL value(s). No effect on /health. |
MCP_HTTP_ALLOWED_HOSTS is compared against the raw Host header, which includes the port. The default already covers loopback access on the configured MCP_HTTP_PORT (127.0.0.1:PORT, localhost:PORT, [::1]:PORT) plus the bare hostnames, which match a portless Host — a client or reverse proxy that omits the port (as on ports 80/443). When you set it explicitly, list the exact Host the client (or your reverse proxy) sends — e.g. app.example.com if the proxy forwards Host: app.example.com on 443, or app.example.com:8443 if it forwards a port.
Every present Origin on /mcp is validated in all modes; an absent Origin remains valid for non-browser clients. In non-hardened mode, an unset MCP_HTTP_ALLOWED_ORIGINS defaults to the exact HTTP/HTTPS loopback origins http://127.0.0.1, https://127.0.0.1, http://localhost, https://localhost, http://[::1], and https://[::1], both portless and with the configured MCP_HTTP_PORT. A non-empty MCP_HTTP_ALLOWED_ORIGINS replaces those defaults. Entries are trimmed but otherwise literal; matching is exact, case-sensitive literal matching, including scheme and port. Malformed, scheme-less, path-bearing, trailing slash, or differently-cased values silently do not match and must be corrected. Hardened mode still requires an explicit allowlist and adds authentication plus Host enforcement. An invalid present Origin on /mcp receives a fixed, non-reflecting 403 before parser, authentication, rate limiting, or transport construction. /health is outside the MCP 403 boundary but uses the narrowed global CORS allowlist. Before upgrading, existing non-hardened browser deployments using non-loopback Origins must set MCP_HTTP_ALLOWED_ORIGINS or receive a fixed 403. The transport support matrix above defines the modern sessionless and retained legacy boundaries.
web_url_read blocks private/internal URLs by default in all transport modes. This includes localhost, loopback addresses, private IPv4 ranges, link-local addresses, 0.0.0.0/8, CGNAT (100.64.0.0/10), IANA special-purpose IPv4 ranges, IPv6 loopback/ULA/link-local addresses, and IPv4-mapped IPv6 private addresses.
Redirects are also checked before they are followed. A public URL that redirects to a private/internal URL is blocked.
For direct URL-reader requests without a proxy, DNS answers are validated before connecting. A public-looking hostname that resolves to a private/internal address is blocked, and the connection is pinned to the validated DNS answer to prevent DNS rebinding between validation and connection.
When a URL-reader proxy is configured (URL_READER_HTTP_PROXY, URL_READER_HTTPS_PROXY, HTTP_PROXY, or HTTPS_PROXY), the proxy performs DNS resolution. Client-side DNS-answer validation cannot inspect proxied resolutions, so proxied deployments should rely on proxy, firewall, and egress controls.
URL_READ_MAX_CONTENT_LENGTH_BYTES is enforced while streaming the response
body, including chunked responses and responses whose GET body is larger than
the HEAD Content-Length value. The limit is measured after transparent
response decompression.
For application/pdf, both the downloaded input and extracted UTF-8 text are
limited to the lower of this value and 16 MiB; extraction is limited to
500 pages and does not perform OCR. A response declared as PDF must begin with the
%PDF- signature or it returns a type-mismatch explanation without entering
the parser.
At most two PDF extractions run concurrently per MCP process. There is no
queue; additional concurrent reads return
PDF text extraction is busy; try again later. and may be retried. The
two-worker limit is fixed and is not configurable. Each worker has a 192 MiB
V8 old-generation ceiling and a 4 MiB stack ceiling. These are engine limits
rather than reserved memory, a complete process-memory limit, or an
operating-system sandbox.
PDF parsing starts only after the response body is complete and has its own
30-second worker budget. On the direct path, the HEAD checks and response body
share the configured FETCH_TIMEOUT_MS network budget, after which parsing can
take up to 30 additional seconds. With a browser solver enabled, add the initial
HEAD preflight (up to 3 seconds), solver acquisition (the selected provider
timeout plus up to 5 response-transfer seconds), and then the same replay-fetch
and parser budgets.
Set MCP_HTTP_ALLOW_PRIVATE_URLS=true only when internal URL reads are intentional for your deployment. This also allows hostnames that DNS-resolve to private/internal addresses.
This combined MCP client configuration shows the supported option groups in one place. Remove settings you do not need. Some options have dependencies: MCP_HTTP_HARDEN=true, MCP_HTTP_AUTH_TOKEN, and MCP_HTTP_ALLOWED_ORIGINS must be configured together; MCP_HTTP_ALLOWED_HOSTS and MCP_HTTP_TRUST_PROXY depend on the exact network and proxy topology. The representative MCP_HTTP_TRUST_PROXY=1 value assumes exactly one trusted proxy hop. Without that trusted proxy boundary, clients can spoof X-Forwarded-For and influence IP-based rate limiting and logs.
{
"mcpServers": {
"searxng": {
"command": "npx",
"args": ["-y", "mcp-searxng"],
"env": {
"SEARXNG_URL": "https://searxng.example.com",
"AUTH_USERNAME": "legacy-fallback-user",
"AUTH_PASSWORD": "legacy-fallback-password",
"SEARXNG_FANOUT": "false",
"SEARXNG_TIMEOUT_MS": "10000",
"SEARXNG_MAX_RESPONSE_BYTES": "5242880",
"FETCH_TIMEOUT_MS": "10000",
"SEARXNG_LITE_TOOLS": "false",
"SEARXNG_DEFAULT_LANGUAGE": "en",
"SEARXNG_DEFAULT_SAFESEARCH": "0",
"SEARXNG_DEFAULT_RESPONSE_FORMAT": "text",
"SEARXNG_MAX_RESULTS": "10",
"SEARXNG_MAX_RESULT_CHARS": "500",
"SEARCH_CACHE_TTL_MS": "86400000",
"SEARCH_CACHE_MAX_ENTRIES": "200",
"SEARXNG_HTML_FALLBACK": "false",
"URL_READ_MAX_CHARS": "2000",
"URL_READ_MAX_CONTENT_LENGTH_BYTES": "5242880",
"FLARESOLVERR_URL": "http://flaresolverr:8191",
"FLARESOLVERR_TIMEOUT_MS": "60000",
"FLARESOLVERR_MAX_CONCURRENT_REQUESTS": "2",
"CACHE_TTL_MS": "86400000",
"CACHE_MAX_ENTRIES": "500",
"USER_AGENT": "MyBot/1.0",
"SEARCH_USER_AGENT": "MySearchBot/1.0",
"URL_READER_USER_AGENT": "Mozilla/5.0 (compatible; MyBot/1.0)",
"SEARCH_HTTP_PROXY": "http://search-proxy.company.com:8080",
"SEARCH_HTTPS_PROXY": "http://search-proxy.company.com:8080",
"URL_READER_HTTP_PROXY": "http://reader-proxy.company.com:8080",
"URL_READER_HTTPS_PROXY": "http://reader-proxy.company.com:8080",
"HTTP_PROXY": "http://global-proxy.company.com:8080",
"HTTPS_PROXY": "http://global-proxy.company.com:8080",
"NO_PROXY": "localhost,127.0.0.1,.local,.internal",
"MCP_HTTP_PORT": "3000",
"MCP_HTTP_HOST": "0.0.0.0",
"MCP_HTTP_TRUST_PROXY": "1",
"MCP_HTTP_STATELESS": "false",
"MCP_HTTP_STATELESS_MAX_IN_FLIGHT": "16",
"MCP_HTTP_STATELESS_MAX_IN_FLIGHT_PER_IP": "8",
"MCP_HTTP_STATELESS_REQUEST_TIMEOUT_MS": "900000",
"MCP_RATE_WINDOW_MS": "60000",
"MCP_RATE_INIT_MAX": "20",
"MCP_RATE_SESSION_MAX": "300",
"MCP_HTTP_HARDEN": "true",
"MCP_HTTP_AUTH_TOKEN": "replace-me",
"MCP_HTTP_ALLOWED_ORIGINS": "https://app.example.com",
"MCP_HTTP_ALLOWED_HOSTS": "app.example.com",
"MCP_HTTP_ALLOW_PRIVATE_URLS": "false",
"MCP_HTTP_EXPOSE_FULL_CONFIG": "false"
}
}
}
}