Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
30 changes: 30 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,36 @@ A running MCPProxy core watches `mcp_config.json` and hot-reloads external edits
| `call_tool_timeout` | string | `"2m"` | Timeout for tool calls (e.g., `"30s"`, `"2m"`, `"5m"`). **Note**: When using agents like Codex or Claude as MCP servers, you may need to increase this timeout significantly, even up to 10 minutes (`"10m"`), as these agents may require longer processing times for complex operations |
| `init_timeout` | duration | `"30s"` | Deadline for an upstream's MCP `initialize` handshake (e.g. `"30s"`, `"120s"`, `"3m"`). Raise this for servers that do legitimate first-run warmup — building a cache/index or prefetching — before they answer `initialize`, so they are not killed mid-startup. Global default; can be overridden per server (see [Server Fields](#server-fields)). Range: `1s`–`30m`; `"0s"`/unset uses the 30s default. |

### HTTP Server Timeouts

Deadlines applied to mcpproxy's own HTTP listener (REST API, `/mcp`, `/events`).
These are separate from `call_tool_timeout`, which caps how long an *upstream
tool* may run.

```json
{
"http_read_timeout": "120s",
"http_write_timeout": "120s",
"http_idle_timeout": "180s"
}
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `http_read_timeout` | duration | `"120s"` | Deadline for reading the entire request (headers + body). `"0s"` = no timeout. Range: `1s`–`24h`. |
| `http_write_timeout` | duration | `"120s"` | Wall-clock cap on writing the entire response, counted from when the request headers were read. **Governs non-streaming endpoints only** (REST API, Web UI, health) — the MCP endpoints and the SSE `/events` stream are exempt by design (see below, [#965](https://github.com/smart-mcp-proxy/mcpproxy-go/issues/965)). `"0s"` disables it globally. Range: `1s`–`24h`. |
| `http_idle_timeout` | duration | `"180s"` | Keep-alive timeout for idle persistent connections. `"0s"` removes the dedicated idle deadline, but Go's `net/http` then falls back to the read timeout — idle is fully unbounded only when `http_read_timeout` is also `"0s"`. Range: `1s`–`24h`. |

Notes:

- **Streaming routes are exempt from `http_write_timeout`.** A write deadline caps the whole response, so it would truncate any tool call slower than it and silently kill long-lived SSE streams. The MCP endpoints (`/mcp`, `/mcp/all`, `/mcp/code`, `/mcp/call`, `/mcp/p/<slug>`, plus the legacy `/v1/tool_code` and `/v1/tool-code` aliases) and `/events` therefore clear their own per-request write deadline (and, being body-less GETs, their read deadline). Everything else keeps the configured deadline, which is what protects a non-loopback deployment from slow readers. You do **not** need to disable `http_write_timeout` to run long tool calls ([#965](https://github.com/smart-mcp-proxy/mcpproxy-go/issues/965)).
- **`"0s"` means "no timeout"**, not "use the default" — unlike `init_timeout`. Omit the key entirely to get the built-in default. Setting `http_write_timeout` to `"0s"` removes the deadline from *every* endpoint, including REST/UI/health. Exception: `http_idle_timeout: "0s"` alone does not unbound idle connections — Go's `net/http` falls back to the read timeout (see the field row above).
- **A restart is required.** These values are baked into the HTTP server when it binds, so a config edit is reported as restart-required rather than hot-reloaded.
- **Slowloris protection is unaffected**: the 60s request-header read deadline is hardcoded and not configurable.
- **Long tool calls need `call_tool_timeout`.** It (default `"2m"`) separately caps tool execution. To allow tool calls longer than two minutes, raise `call_tool_timeout` — the MCP routes' write-deadline exemption alone is not enough.

Environment overrides: `MCPPROXY_HTTP_READ_TIMEOUT`, `MCPPROXY_HTTP_WRITE_TIMEOUT`, `MCPPROXY_HTTP_IDLE_TIMEOUT`.

### TOON Output (Adaptive Result Encoding)

```json
Expand Down
24 changes: 24 additions & 0 deletions docs/configuration/config-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ MCPProxy uses a JSON configuration file located at `~/.mcpproxy/mcp_config.json`
"enable_socket": true,
"health_check_interval": "30s",
"tool_discovery_interval": "5m",
"http_read_timeout": "120s",
"http_write_timeout": "120s",
"http_idle_timeout": "180s",
"tools_limit": 15,
"tool_response_limit": 20000,
"enable_code_execution": false,
Expand Down Expand Up @@ -59,6 +62,25 @@ MCPProxy uses a JSON configuration file located at `~/.mcpproxy/mcp_config.json`
| `require_mcp_auth` | boolean | `false` | Require an API key on the `/mcp` endpoint (off by default for client compatibility). Enable when exposing MCPProxy beyond localhost |
| `enable_socket` | boolean | `true` | Enable Unix socket/named pipe for local communication |

### HTTP Server Timeouts

Deadlines applied to MCPProxy's own HTTP listener (REST API, `/mcp`, `/events`).
Each accepts a duration string; **`"0s"` means "no timeout"** (not "use the
default" — omit the key for that; for `http_idle_timeout`, `"0s"` falls back to
the read timeout — see its row). Valid range: `1s`–`24h`, or `0s`.

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `http_read_timeout` | duration | `"120s"` | Deadline for reading the whole request (headers + body) |
| `http_write_timeout` | duration | `"120s"` | Wall-clock cap on writing the whole response, counted from when the request headers were read. Governs **non-streaming endpoints only** (REST API, Web UI, health); MCP endpoints and SSE `/events` are exempt by design. `"0s"` disables it globally ([#965](https://github.com/smart-mcp-proxy/mcpproxy-go/issues/965)) |
| `http_idle_timeout` | duration | `"180s"` | Keep-alive timeout for idle persistent connections (`"0s"` falls back to the read timeout; unbounded only if that is also `"0s"`) |

- **Streaming routes are exempt from `http_write_timeout`.** The MCP endpoints (`/mcp*`, plus the legacy `/v1/tool_code` and `/v1/tool-code` aliases) and `/events` clear their own per-request write deadline (and, being body-less GETs, their read deadline), so a slow tool call or a long-lived SSE stream is never truncated. You do not need to disable the deadline to run long tool calls.
- **Restart required.** These are baked into the HTTP server when it binds, so a change is reported as restart-required, not hot-reloaded.
- **Slowloris protection is unaffected** — the 60s request-header read deadline is hardcoded and not configurable.
- **Long tool calls need `call_tool_timeout`.** It (default `2m`) separately caps tool execution; raise it when you expect tool calls longer than two minutes.
- Environment overrides: `MCPPROXY_HTTP_READ_TIMEOUT`, `MCPPROXY_HTTP_WRITE_TIMEOUT`, `MCPPROXY_HTTP_IDLE_TIMEOUT`.

### Feature Flags

| Option | Type | Default | Description |
Expand Down Expand Up @@ -204,6 +226,8 @@ See [Upstream Servers](/configuration/upstream-servers) for detailed server conf

MCPProxy watches the configuration file for changes and automatically reloads when modifications are detected. No restart is required for most configuration changes.

Exceptions that require a restart include `listen`, `data_dir`, `api_key`, the TLS block, and the three `http_*_timeout` options.

## Environment Variable Overrides

Configuration options can be overridden using environment variables. See [Environment Variables](/configuration/environment-variables) for details.
19 changes: 19 additions & 0 deletions docs/configuration/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,25 @@ set and per-server overrides are file/API-configured. See
| `MCPPROXY_QUEUE_SIZE` | How many calls may wait for a global slot. `0` = shed immediately at the cap | `0` |
| `MCPPROXY_QUEUE_TIMEOUT` | How long a call may wait before being shed, e.g. `30s` | `30s` when the limiter is active |

### HTTP Server Timeouts

Deadlines on MCPProxy's own HTTP listener (REST API, `/mcp`, `/events`). Each
takes a duration string; **`0s` means "no timeout"** (unset means "use the
default"; for the idle timeout, `0s` falls back to the read timeout — see its
row). Valid range: `1s`–`24h`, or `0s`. Malformed values are ignored with a
warning on stderr. Changing any of these requires a restart. See
[HTTP Server Timeouts](./config-file.md#http-server-timeouts).

| Variable | Description | Default |
|----------|-------------|---------|
| `MCPPROXY_HTTP_READ_TIMEOUT` | Deadline for reading the whole request (headers + body) | `120s` |
| `MCPPROXY_HTTP_WRITE_TIMEOUT` | Wall-clock cap on writing the whole response for non-streaming endpoints (REST, Web UI, health). MCP endpoints and SSE `/events` are exempt by design, so slow tool calls and event streams are never truncated; `0s` disables it globally ([#965](https://github.com/smart-mcp-proxy/mcpproxy-go/issues/965)) | `120s` |
| `MCPPROXY_HTTP_IDLE_TIMEOUT` | Keep-alive timeout for idle persistent connections (`0s` falls back to the read timeout; unbounded only if that is also `0s`) | `180s` |

The 60s request-header read deadline (slowloris protection) is hardcoded and not
configurable. `call_tool_timeout` (default `2m`) separately caps tool execution —
raise it too when you expect tool calls longer than two minutes.

### Core Server Examples

```bash
Expand Down
108 changes: 108 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,72 @@ const (
// `initialize` handshake when no per-server or global override is set
// (MCP-3322 / GH #760). It preserves the historical ~30s behaviour.
defaultInitTimeout = 30 * time.Second

// Built-in defaults for the HTTP server's request deadlines (GH #965).
// Like the intervals above they live here (not in DefaultConfig) so an
// unset key resolves to the built-in behaviour and existing configs are
// unchanged.
defaultHTTPReadTimeout = 120 * time.Second
// defaultHTTPWriteTimeout stays at 120s (GH #965). A write deadline is a
// wall-clock cap on the ENTIRE response counted from the moment the request
// headers were read, so it must NOT apply to long tool calls or event
// streams — but it is real slow-reader protection for everything else, and
// dropping it globally would strip that protection from REST, Web UI and
// health endpoints on non-loopback deployments.
//
// The streaming routes are exempted per-request instead: the MCP endpoints
// (/mcp*, plus the legacy /v1/tool_code and /v1/tool-code aliases) and the
// SSE /events stream clear their own write deadline via
// http.ResponseController, so this default never truncates them. Setting the
// key to "0s" still disables the deadline globally.
defaultHTTPWriteTimeout = 120 * time.Second
defaultHTTPIdleTimeout = 180 * time.Second
)

// resolveHTTPTimeout applies the tri-state contract shared by the three HTTP
// server deadlines (GH #965): nil = the built-in default, a pointer to 0 =
// DISABLED (net/http's zero value means "no deadline" — except IdleTimeout,
// where net/http falls back to ReadTimeout; see ResolveHTTPIdleTimeout), a
// positive value = that
// value. A negative value falls back to the default — validation rejects those
// anyway, this only keeps a hand-edited file from producing a nonsense deadline.
//
// Note the deliberate asymmetry with ResolveInitTimeout, where 0 maps back to
// the default: a connect handshake must always have a ceiling, whereas "no
// response deadline at all" is a legitimate HTTP setting an operator may want.
func resolveHTTPTimeout(v *Duration, def time.Duration) time.Duration {
if v == nil {
return def
}
if d := v.Duration(); d >= 0 {
return d
}
return def
}

// ResolveHTTPReadTimeout resolves http.Server.ReadTimeout: unset → 120s,
// 0 → disabled, positive → that value (GH #965).
func (c *Config) ResolveHTTPReadTimeout() time.Duration {
return resolveHTTPTimeout(c.HTTPReadTimeout, defaultHTTPReadTimeout)
}

// ResolveHTTPWriteTimeout resolves http.Server.WriteTimeout: unset → 120s,
// 0 → disabled, positive → that value (GH #965). Streaming routes (MCP + SSE
// /events) clear the resulting deadline per-request, so this value only
// governs non-streaming endpoints — see defaultHTTPWriteTimeout.
func (c *Config) ResolveHTTPWriteTimeout() time.Duration {
return resolveHTTPTimeout(c.HTTPWriteTimeout, defaultHTTPWriteTimeout)
}

// ResolveHTTPIdleTimeout resolves http.Server.IdleTimeout: unset → 180s,
// 0 → no idle deadline of its own, positive → that value (GH #965). NOTE:
// net/http falls back to ReadTimeout when IdleTimeout is zero, so an explicit
// "0s" here fully disables the idle deadline only when http_read_timeout is
// also 0 — otherwise idle connections are reaped after the read timeout.
func (c *Config) ResolveHTTPIdleTimeout() time.Duration {
return resolveHTTPTimeout(c.HTTPIdleTimeout, defaultHTTPIdleTimeout)
}

// resolveInterval applies the per-server → global → default precedence for an
// optional *Duration. A non-nil pointer wins at each level, including a pointer
// to 0 ("disabled"). Returns the resolved duration; a value <= 0 means the
Expand Down Expand Up @@ -227,6 +291,38 @@ type Config struct {
// raise this so they are not killed mid-startup.
InitTimeout *Duration `json:"init_timeout,omitempty" mapstructure:"init-timeout" swaggertype:"string"`

// HTTP server request deadlines (GH #965). *Duration tri-state: nil =
// inherit the built-in default; a pointer to 0s = DISABLED (no deadline —
// with the idle-timeout caveat noted on HTTPIdleTimeout); a positive value
// = that deadline. Validated to {0} ∪ [1s, 24h].
//
// Unlike init_timeout, an explicit 0 here is a SUPPORTED value, not a
// synonym for the default: net/http treats a zero deadline as "no timeout".
// Long-running tool calls and the SSE /events stream do not need that
// escape hatch, though — the MCP endpoints (/mcp*, plus the legacy
// /v1/tool_code and /v1/tool-code aliases) and /events clear their own
// per-request write deadline via http.ResponseController, so the write
// default only governs non-streaming endpoints (REST, Web UI, health).
//
// These are baked into http.Server at bind time, so changing any of them
// REQUIRES A RESTART (DetectConfigChanges reports it as such). Resolved by
// ResolveHTTPReadTimeout / ResolveHTTPWriteTimeout / ResolveHTTPIdleTimeout.
// Note that call_tool_timeout separately caps tool execution (default 2m):
// raise it too when allowing tool calls longer than two minutes.

// HTTPReadTimeout caps how long reading a whole request (headers + body)
// may take. Unset = 120s; "0s" disables it. Requires a restart.
HTTPReadTimeout *Duration `json:"http_read_timeout,omitempty" mapstructure:"http-read-timeout" swaggertype:"string"`
// HTTPWriteTimeout caps how long producing a whole response may take on
// non-streaming endpoints (REST, Web UI, health). Unset = 120s; "0s"
// disables it globally. MCP and SSE /events routes are exempt by design.
HTTPWriteTimeout *Duration `json:"http_write_timeout,omitempty" mapstructure:"http-write-timeout" swaggertype:"string"`
// HTTPIdleTimeout caps how long an idle keep-alive connection is kept open.
// Unset = 180s. "0s" removes the dedicated idle deadline, but net/http then
// falls back to ReadTimeout — idle is fully unbounded only when
// http_read_timeout is also "0s". Requires a restart.
HTTPIdleTimeout *Duration `json:"http_idle_timeout,omitempty" mapstructure:"http-idle-timeout" swaggertype:"string"`

// Environment configuration for secure variable filtering
Environment *secureenv.EnvConfig `json:"environment,omitempty" mapstructure:"environment"`

Expand Down Expand Up @@ -2021,6 +2117,18 @@ func (c *Config) ValidateDetailed() []ValidationError {
errors = append(errors, *e)
}

// HTTP server request deadlines (GH #965). {0} ∪ [1s, 24h]; 0 means
// "no deadline", which validateIntervalBound already accepts.
if e := validateIntervalBound("http_read_timeout", c.HTTPReadTimeout, time.Second, 24*time.Hour); e != nil {
errors = append(errors, *e)
}
if e := validateIntervalBound("http_write_timeout", c.HTTPWriteTimeout, time.Second, 24*time.Hour); e != nil {
errors = append(errors, *e)
}
if e := validateIntervalBound("http_idle_timeout", c.HTTPIdleTimeout, time.Second, 24*time.Hour); e != nil {
errors = append(errors, *e)
}

// Concurrency limits, all three scopes after resolution (spec 093, FR-023).
errors = append(errors, c.validateConcurrency()...)

Expand Down
Loading
Loading