Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,11 @@ Never set `NODE_TLS_REJECT_UNAUTHORIZED=0`. It disables all TLS certificate vali

## HTTP Transport

By default the server communicates over STDIO. Set `MCP_HTTP_PORT` to enable HTTP mode instead.
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.

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 |
|---|---|---|---|
Expand All @@ -324,13 +328,14 @@ By default the server communicates over STDIO. Set `MCP_HTTP_PORT` to enable HTT
| `MCP_HTTP_STATELESS_REQUEST_TIMEOUT_MS` | No | `900000` (range `1000`-`2147483647`) | Maximum lifetime of an admitted stateless POST, including server construction, MCP handling, and an active response stream. |

**HTTP endpoints (when HTTP mode is active):**
- Stateful default: `POST/GET/DELETE /mcp` — session-based MCP protocol
- Modern: `POST /mcp` — sessionless MCP protocol
- Legacy stateful default: `POST/GET/DELETE /mcp` — session-based MCP protocol
- With `MCP_HTTP_STATELESS=true`: `POST /mcp` only; GET and DELETE return HTTP 405 with `Allow: 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. This mode does not require an SDK 2.0 upgrade; it uses the stateless transport contract provided by the currently supported SDK.
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 (HTTP mode)

Expand Down Expand Up @@ -415,7 +420,7 @@ Opt-in security layer for when you expose the HTTP transport on a network. Defau

### Origin validation and upgrade notice

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. This change addresses the Origin security requirement; legacy transport compatibility remains and is not a claim of full MCP 2026-07-28 transport compliance.
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.

## URL Reader Security

Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ For measured MCP-process CPU and memory starting points, see
- **Browser Solver Support**: For each uncached URL that passes static URL validation and the HEAD size preflight, optionally acquire a browser session from FlareSolverr, Byparr, or both, then replay the returned user-agent and scoped cookies through the bounded URL reader. In dual-provider mode FlareSolverr is always primary and Byparr is attempted only after a busy or transient-unavailable primary. FlareSolverr 3.5.0 and Byparr 2.1.0 were verified on 2026-07-30.
- **Intelligent Caching**: Both search results and URL content are cached in memory with configurable TTL and least-frequently-used (LFU) eviction, reducing redundant requests.
- **SSRF Protection**: `web_url_read` blocks private/internal URLs and redirects by default in all transport modes.
- **HTTP Transport**: Optional Streamable HTTP mode with opt-in hardening, rate limiting, and bounded stateless compatibility for serverless or horizontally scaled deployments.
- **HTTP Transport**: Optional MCP SDK v2 Streamable HTTP mode with opt-in hardening, rate limiting, and bounded stateless compatibility for serverless or horizontally scaled deployments. Modern 2026-07-28 requests and retained legacy clients share the same tool and resource surface.
- **HTML Fallback**: Optionally parse results from the HTML page for public instances that reject `format=json`.
- **Lite Tools Mode**: Minimal tool schemas for local models with small context windows.
- **Proxy Support**: Global or per-tool HTTP/HTTPS proxies for search and URL-reader traffic.
Expand Down Expand Up @@ -340,13 +340,15 @@ The `--add-host` mapping lets the container reach a SearXNG instance on the host
}
```

**Endpoints:** `POST/GET/DELETE /mcp` (stateful MCP protocol), `GET /health` (health check)
**Protocol support:** HTTP and STDIO serve modern MCP `2026-07-28` and the retained legacy revisions `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05`, and `2024-10-07`. Modern HTTP is sessionless `POST /mcp`; legacy HTTP remains stateful by default (`POST/GET/DELETE /mcp`) or uses the existing POST-only stateless mode.

Stateful sessions remain the default. Set `MCP_HTTP_STATELESS=true` when a deployment cannot preserve in-memory sessions between requests. Every stateless POST creates a fresh MCP server and transport, ignores any incoming session ID, and returns negotiated JSON or an SSE stream within that same POST. Stateless mode is POST-only: `GET /mcp` and `DELETE /mcp` return HTTP 405 with `Allow: POST`, and no cross-request subscriptions, resumability, or server-to-client notifications are preserved.
**Endpoints:** modern `POST /mcp`; legacy `POST/GET/DELETE /mcp` in the stateful default, or legacy `POST /mcp` only with stateless mode; `GET /health`.

For legacy HTTP clients, stateful sessions remain the default. Set `MCP_HTTP_STATELESS=true` when a deployment cannot preserve in-memory legacy sessions between requests. Modern HTTP remains sessionless regardless of that setting. Every stateless POST creates a fresh MCP server and transport, ignores any incoming session ID, and returns negotiated JSON or an SSE stream within that same POST. Stateless mode is POST-only: `GET /mcp` and `DELETE /mcp` return HTTP 405 with `Allow: POST`, and no cross-request subscriptions, resumability, or server-to-client notifications are preserved.

Stateless requests are bounded by global and per-client-IP in-flight limits plus a request lifetime. See [CONFIGURATION.md](CONFIGURATION.md#http-transport) for defaults, overload and timeout responses, proxy-aware fairness, and the complete compatibility contract.

**Origin validation and upgrade notice:** 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. This change addresses the Origin security requirement; legacy transport compatibility remains and is not a claim of full MCP 2026-07-28 transport compliance.
**Origin validation and upgrade notice:** 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.

**Test it:**

Expand Down
8 changes: 6 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,17 @@ Hardened mode protects the MCP protocol endpoint (`/mcp`) with:
- **Origin allowlist** — `/mcp` requests from unlisted browser origins are rejected
- **DNS rebinding protection** — the `Host` header is validated against `MCP_HTTP_ALLOWED_HOSTS`. The default allows loopback access on the configured port (`127.0.0.1`, `localhost`, `[::1]` and their `:PORT` forms). A custom list is matched exactly against `Host`: an entry matches only when it carries the same port the client or proxy sends (e.g. `app.example.com:8443`).

With `MCP_HTTP_STATELESS=true`, bearer authorization and the hardened Host and Origin checks run before any per-request MCP server is constructed. Rate limiting also runs before construction, followed by the stateless capacity controls.
With `MCP_HTTP_STATELESS=true`, bearer authorization and the hardened Host and Origin checks run before any per-request MCP server is constructed. Rate limiting also runs before construction, followed by the stateless capacity controls. Modern and legacy stateless requests share that capacity, while `/health` and MCP control traffic remain available during tool-admission exhaustion.

HTTP and STDIO support modern `2026-07-28` and legacy `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05`, and `2024-10-07`. Modern HTTP is sessionless POST-only; legacy stateful and stateless behavior retains the documented session and 405 boundaries.

Modern HTTP clients are expected to provide `MCP-Protocol-Version`. For the published SDK `2.0.0`, a narrow temporary guard rejects a safe 2026-07-28 request shape when that header is missing; maintainers can remove the guard after proving that a stable SDK containing upstream PR 2594 provides the same contract.

`MCP_HTTP_HARDEN=true` will fail to start if `MCP_HTTP_AUTH_TOKEN` or `MCP_HTTP_ALLOWED_ORIGINS` are missing.

### Origin validation and upgrade notice

When an `/mcp` request includes an `Origin`, the server validates it against the applicable allowlist in hardened and non-hardened modes; an absent `Origin` remains accepted 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. This change addresses the Origin security requirement; legacy transport compatibility remains and is not a claim of full MCP 2026-07-28 transport compliance.
When an `/mcp` request includes an `Origin`, the server validates it against the applicable allowlist in hardened and non-hardened modes; requests without `Origin` follow the accepted non-browser path. 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 and case-sensitive, including scheme and port. Malformed, scheme-less, path-bearing, trailing slash, or differently-cased values do not match and require correction. Hardened mode 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, non-hardened browser deployments using non-loopback Origins should set `MCP_HTTP_ALLOWED_ORIGINS`; otherwise those requests receive a fixed 403.

`GET /health` intentionally remains unauthenticated so container orchestrators
and load balancers can perform liveness checks. It is independently limited to
Expand Down
3 changes: 1 addition & 2 deletions __tests__/e2e/http-transport.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
#!/usr/bin/env tsx

import { strict as assert } from 'node:assert';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { createServer } from 'node:net';
import { fileURLToPath } from 'node:url';
import { checkSkipConditions, spawnHttpCli, type SpawnedHttpCli } from './helpers/spawn-server.js';
Expand Down
Loading