Skip to content

Commit c06d13b

Browse files
authored
Reconcile stdin config support and developer docs with implementation (#5645)
## Bug Fix The nightly documentation reconciliation found several drifts between the documented config surface and the actual implementation. The main gaps were missing JSON stdin support for `gateway.payloadPathPrefix`, inconsistent stdin server timeout field naming, and contributor docs that no longer matched the `internal/config` package layout or validation rules. ### What was the bug? - `AGENTS.md` implied a single `internal/config/config.go` entry point even though the config package is split across multiple files. - JSON stdin config could not set `gateway.payloadPathPrefix`, despite TOML and env/flag paths supporting it. - Per-server stdin timeout fields used `snake_case` while the rest of the JSON gateway config uses `camelCase`. - Docs/examples were missing or outdated for: - absolute-path requirements on `payload_dir` - OpenTelemetry vs legacy tracing key precedence - integration-test env vars `AWMG_BINARY_PATH` and `AWMG_WASM_GUARD_PATH` ### How did you fix it? - **JSON stdin config parity** - Added `payloadPathPrefix` to `StdinGatewayConfig` and wired it into stdin-to-runtime config conversion. - Standardized stdin server timeout fields on `connectTimeout` / `toolTimeout`. - Kept `connect_timeout` / `tool_timeout` as backward-compatible JSON aliases during unmarshalling. - Updated the stdin JSON schema and config tests to cover the new field and alias behavior. - **Developer docs reconciliation** - Updated `AGENTS.md` to describe the real multi-file `internal/config` layout, including `config_core.go`, `config_stdin.go`, feature-specific config files, and split validation files. - Documented that `payload_dir` / `MCP_GATEWAY_PAYLOAD_DIR` must be absolute in examples and reference docs. - Added migration guidance for `[gateway.opentelemetry]` vs legacy `[gateway.tracing]`, including precedence when both are present. - Added integration test environment variable docs to `CONTRIBUTING.md`. - **Reference/example cleanup** - Updated config examples and docs to reflect the supported JSON field names and current behavior. - Added `payloadPathPrefix` to the configuration reference alongside existing payload settings. ### Testing - Added/updated focused config tests for: - `gateway.payloadPathPrefix` in stdin JSON - camelCase per-server timeout fields - backward-compatible snake_case timeout aliases ```json { "gateway": { "apiKey": "${MCP_GATEWAY_API_KEY}", "payloadPathPrefix": "/workspace/payloads" }, "mcpServers": { "repo-mind": { "type": "http", "url": "http://127.0.0.1:8000/mcp", "connectTimeout": 45, "toolTimeout": 600 } } } ```
2 parents 4663036 + ba1af89 commit c06d13b

13 files changed

Lines changed: 346 additions & 42 deletions

AGENTS.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ Quick reference for AI agents working with MCP Gateway (Go-based MCP proxy serve
2424

2525
- `internal/auth/` - Authentication header parsing and middleware
2626
- `internal/cmd/` - CLI (Cobra)
27-
- `internal/config/` - Config parsing (TOML/JSON) with validation
28-
- `validation.go` - Variable expansion and fail-fast validation
27+
- `internal/config/` - Config parsing (TOML/JSON) with validation; the package is split across multiple files
28+
- `config_core.go` - Core `Config`, `GatewayConfig`, and `ServerConfig` types plus TOML loading
29+
- `config_stdin.go` - JSON stdin structs and stdin→internal config conversion
30+
- `config_env.go`, `config_feature.go`, `config_tracing.go` - Environment- and feature-specific config helpers
31+
- `validation.go`, `validation_env.go`, `validation_schema.go` - Fail-fast field, environment, and schema validation
2932
- `validation_test.go` - Comprehensive validation tests
3033
- `internal/difc/` - Decentralized Information Flow Control
3134
- `internal/envutil/` - Environment variable utilities
@@ -64,7 +67,7 @@ Quick reference for AI agents working with MCP Gateway (Go-based MCP proxy serve
6467
[gateway]
6568
port = 3000
6669
api_key = "your-api-key"
67-
payload_dir = "/tmp/jq-payloads" # Optional: directory for large payload storage
70+
payload_dir = "/tmp/jq-payloads" # Optional: directory for large payload storage (must be absolute)
6871

6972
[servers.github]
7073
command = "docker"
@@ -383,8 +386,8 @@ DEBUG_COLORS=0 DEBUG=* ./awmg --config config.toml
383386
- `DEBUG_COLORS` - Control colored output (0 to disable, auto-disabled when piping)
384387
- `MCP_GATEWAY_LOG_DIR` - Log file directory (sets default for `--log-dir` flag, default: `/tmp/gh-aw/mcp-logs`)
385388
- `MCP_GATEWAY_WASM_CACHE_DIR` - Disk-backed wazero compilation cache directory (sets default for `--wasm-cache-dir`, default: `<log-dir>/wazero-cache`)
386-
- `MCP_GATEWAY_PAYLOAD_DIR` - Large payload storage directory (sets default for `--payload-dir` flag, default: `/tmp/jq-payloads`)
387-
- `MCP_GATEWAY_PAYLOAD_PATH_PREFIX` - Path prefix for remapping payloadPath returned to clients (sets default for `--payload-path-prefix` flag, default: empty)
389+
- `MCP_GATEWAY_PAYLOAD_DIR` - Large payload storage directory (sets default for `--payload-dir` flag, default: `/tmp/jq-payloads`). Must be an absolute path.
390+
- `MCP_GATEWAY_PAYLOAD_PATH_PREFIX` - Path prefix for remapping payloadPath returned to clients (sets default for `--payload-path-prefix` flag, default: empty). In JSON stdin config use `gateway.payloadPathPrefix`.
388391
- `MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD` - Size threshold in bytes for payload storage; payloads larger than this are stored to disk (sets default for `--payload-size-threshold` flag, default: `524288`)
389392
- `MCP_GATEWAY_SESSION_TIMEOUT` - Session timeout for stateful sessions in both unified mode (`/mcp`) and routed mode (`/mcp/<server>`). Accepts Go duration strings (e.g., `30m`, `1h`, `2h30m`). (default: `6h`)
390393
- `MCP_GATEWAY_TOOL_TIMEOUT` - Tool invocation timeout in seconds. Fallback when `gateway.toolTimeout` is not set in stdin JSON config. Accepts any integer ≥ 10 (no upper bound). Priority: stdin config > env var > built-in default. (default: `60`)
@@ -426,7 +429,7 @@ DEBUG_COLORS=0 DEBUG=* ./awmg --config config.toml
426429

427430
**Large Payload Handling:**
428431
- Large tool response payloads are stored in the configured payload directory
429-
- Default payload directory: `/tmp/jq-payloads` (configurable via `--payload-dir` flag, `MCP_GATEWAY_PAYLOAD_DIR` env var, or `payload_dir` in config)
432+
- Default payload directory: `/tmp/jq-payloads` (configurable via `--payload-dir` flag, `MCP_GATEWAY_PAYLOAD_DIR` env var, or `payload_dir` in config). The configured path must be absolute.
430433
- Payloads are organized by session ID: `{payload_dir}/{sessionID}/{queryID}/payload.json`
431434
- This allows agents to mount their session-specific subdirectory to access full payloads
432435
- The jq middleware returns: preview (first `PayloadPreviewSize` chars, default 500), schema, payloadPath, queryID, originalSize, truncated flag

CONTRIBUTING.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ make test-container-proxy
107107

108108
This target builds a Docker image and tests proxy mode with TLS. It requires a GitHub token available via the `gh` CLI (`gh auth login`) or the `GITHUB_TOKEN`/`GH_TOKEN` environment variable.
109109

110+
#### Testing Environment Variables
111+
112+
- `AWMG_BINARY_PATH` — Override the binary path used by integration tests when you want to run tests against a prebuilt `awmg` binary.
113+
- `AWMG_WASM_GUARD_PATH` — Override the GitHub guard WASM path used by proxy integration tests when the default build output path is not available.
114+
110115
#### Race Detection Tests
111116
Run unit tests with Go's race detector to catch concurrent data races:
112117
```bash
@@ -170,8 +175,8 @@ echo '{"mcpServers": {...}}' | ./awmg --config-stdin
170175
# Increase verbosity
171176
./awmg --config config.toml -v
172177

173-
# Custom payload directory and size threshold
174-
./awmg --config config.toml --payload-dir ./payloads --payload-size-threshold 1048576
178+
# Custom payload directory and size threshold (payload dir must be absolute)
179+
./awmg --config config.toml --payload-dir /tmp/payloads --payload-size-threshold 1048576
175180
```
176181

177182
See [docs/ENVIRONMENT_VARIABLES.md](docs/ENVIRONMENT_VARIABLES.md) for the full list of environment variable overrides.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ The gateway starts in routed mode on `http://0.0.0.0:8000`, proxying MCP request
4747
- `-i`: Enables stdin for passing JSON configuration
4848
- `-v /var/run/docker.sock`: Required for spawning backend MCP servers
4949
- `-p 8000:8000`: Port mapping must match `MCP_GATEWAY_PORT`
50+
- If you configure `payloadDir` / `MCP_GATEWAY_PAYLOAD_DIR`, use an absolute path (for example `/tmp/jq-payloads`)
5051

5152
## Guard Policies
5253

config.example-payload-threshold.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ port = 3000
1212
api_key = "your-api-key-here"
1313

1414
# Payload directory for storing large tool responses
15+
# Must be an absolute path
1516
# Can also be set via:
1617
# - Flag: --payload-dir /custom/path
1718
# - Env: MCP_GATEWAY_PAYLOAD_DIR=/custom/path

config.example.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ startup_timeout = 60
3131
tool_timeout = 120
3232

3333
# Directory for storing large payload files (default: /tmp/jq-payloads)
34+
# Must be an absolute path
3435
# Payloads are segmented by session ID: {payload_dir}/{sessionID}/{queryID}/payload.json
3536
# Can also be set via MCP_GATEWAY_PAYLOAD_DIR environment variable or --payload-dir flag
3637
# payload_dir = "/tmp/jq-payloads"
@@ -39,6 +40,11 @@ tool_timeout = 120
3940
# Prevents remote servers from expiring idle sessions by sending periodic pings.
4041
# Set to -1 to disable keepalive pings entirely.
4142
# keepalive_interval = 1500
43+
#
44+
# OpenTelemetry TOML key migration:
45+
# - Prefer [gateway.opentelemetry] for new configs.
46+
# - Legacy [gateway.tracing] is still accepted for TOML migrations.
47+
# - If both sections are present, [gateway.opentelemetry] takes precedence.
4248

4349
# ============================================================================
4450
# MCP Server Configurations

docs/CONFIGURATION.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,11 @@ Run `./awmg --help` for full CLI options. Key flags:
185185
- Enables per-server DIFC guard assignment independent of `guard-policies`
186186
- Example: `guard = "github"` (uses the guard named `github` from `[guards.github]`)
187187

188-
- **`connect_timeout`** (optional, HTTP servers only): Per-transport connection timeout in seconds for connecting to HTTP backends. The gateway tries streamable HTTP, then SSE, then plain JSON-RPC over HTTP POST in sequence; this timeout applies to each attempt. It does **not** set the end-to-end `tools/call` execution timeout. Default: `30`.
188+
- **`connectTimeout`** (preferred JSON) / **`connect_timeout`** (legacy JSON alias, HTTP servers only): Per-transport connection timeout in seconds for connecting to HTTP backends. The gateway tries streamable HTTP, then SSE, then plain JSON-RPC over HTTP POST in sequence; this timeout applies to each attempt. It does **not** set the end-to-end `tools/call` execution timeout. Default: `30`.
189189

190-
- **`tool_timeout`** (optional): Per-server tool invocation timeout in seconds. When set to a positive value, overrides the global tool timeout for `tools/call` requests to this specific server. This allows reusable shared workflow components wrapping long-running HTTP MCP servers to set an appropriate timeout once, without requiring every consumer to configure `MCP_GATEWAY_TOOL_TIMEOUT`. Minimum: `10`. Omit the field (or set to `0`) to fall back to the global timeout.
190+
- **`toolTimeout`** (preferred JSON) / **`tool_timeout`** (legacy JSON alias, optional): Per-server tool invocation timeout in seconds. When set to a positive value, overrides the global tool timeout for `tools/call` requests to this specific server. This allows reusable shared workflow components wrapping long-running HTTP MCP servers to set an appropriate timeout once, without requiring every consumer to configure `MCP_GATEWAY_TOOL_TIMEOUT`. Minimum: `10`. Omit the field (or set to `0`) to fall back to the global timeout.
191191
- Global timeout field name: **`toolTimeout`** in stdin JSON (`gateway.toolTimeout`), **`tool_timeout`** in TOML (`[gateway]` → `tool_timeout`)
192-
- Example (stdin JSON): `"tool_timeout": 600` on an HTTP MCP server that may take up to 10 minutes
192+
- Example (stdin JSON): `"toolTimeout": 600` on an HTTP MCP server that may take up to 10 minutes
193193
- Example (TOML): `tool_timeout = 600` under a `[servers.my-server]` section
194194

195195
- **`rate_limit_threshold`** (optional, TOML/JSON file configs only): Number of consecutive rate-limit errors from this backend that will trip the circuit breaker (transition CLOSED → OPEN). When OPEN, requests are immediately rejected until the breaker is eligible to transition to HALF-OPEN again; this is normally controlled by `rate_limit_cooldown`, but if the gateway knows an upstream rate-limit reset time (for example from response headers or parsed tool error text), that reset time takes precedence. **Not available in JSON stdin format.** Default: `3`.
@@ -394,7 +394,8 @@ The `customSchemas` top-level field allows you to define custom server types bey
394394
| `domain` | Gateway domain (`"localhost"`, `"host.docker.internal"`, or `"${VAR}"`) | (unset) |
395395
| `startupTimeout` | Seconds to wait for backend startup | `30` |
396396
| `toolTimeout` | Maximum seconds for a single tool call, enforced as a context deadline on all backend requests (stdio and HTTP) | `60` |
397-
| `payloadDir` | Directory for large payload files | `/tmp/jq-payloads` |
397+
| `payloadDir` | Directory for large payload files. Must be an absolute path. | `/tmp/jq-payloads` |
398+
| `payloadPathPrefix` | Optional path prefix used when returning `payloadPath` values to clients (for example when the host payload directory is mounted at a different in-container path) | (empty - use actual filesystem path) |
398399
| `payloadSizeThreshold` (JSON) / `payload_size_threshold` (TOML) | Size threshold in bytes; responses larger than this are stored to disk and returned as a `payloadPath` reference | `524288` (512 KB) |
399400
| `trustedBots` (JSON) / `trusted_bots` (TOML) | Optional list of additional bot usernames to trust with "approved" integrity level. Additive to the built-in trusted bot list. When specified, must be a non-empty array with non-empty string entries (spec §4.1.3.4); omit the field entirely if not needed. Example: `["my-bot[bot]", "org-automation"]` | (disabled) |
400401
| `keepaliveInterval` (JSON) / `keepalive_interval` (TOML) | Interval (seconds) between keepalive pings sent to HTTP backends. Prevents remote servers from expiring idle sessions. Set to `-1` to disable keepalive pings entirely. | `1500` (25 min) |

docs/ENVIRONMENT_VARIABLES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ When running locally (`run.sh`), these variables are optional (warnings shown if
2323
| `MCP_GATEWAY_API_KEY` | Informational only — not read directly by the binary; must be referenced in your config via `"${MCP_GATEWAY_API_KEY}"` to enable authentication | (disabled) |
2424
| `MCP_GATEWAY_LOG_DIR` | Log file directory (sets default for `--log-dir` flag) | `/tmp/gh-aw/mcp-logs` |
2525
| `MCP_GATEWAY_WASM_CACHE_DIR` | Disk-backed wazero compilation cache directory (sets default for `--wasm-cache-dir`; defaults to `<log-dir>/wazero-cache`) | `/tmp/gh-aw/mcp-logs/wazero-cache` |
26-
| `MCP_GATEWAY_PAYLOAD_DIR` | Large payload storage directory (sets default for `--payload-dir` flag) | `/tmp/jq-payloads` |
26+
| `MCP_GATEWAY_PAYLOAD_DIR` | Large payload storage directory (sets default for `--payload-dir` flag). Must be an absolute path. | `/tmp/jq-payloads` |
2727
| `MCP_GATEWAY_PAYLOAD_PATH_PREFIX` | Path prefix for remapping payloadPath returned to clients (sets default for `--payload-path-prefix` flag) | (empty - use actual filesystem path) |
2828
| `MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD` | Size threshold in bytes for payload storage (sets default for `--payload-size-threshold` flag) | `524288` |
2929
| `MCP_GATEWAY_SESSION_TIMEOUT` | Session timeout for stateful sessions in both unified (`/mcp`) and routed (`/mcp/<server>`) modes. Accepts Go duration strings (e.g., `30m`, `1h`). Default is 6 hours to match the GitHub Actions default timeout. | `6h` |

internal/config/config_stdin.go

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type StdinGatewayConfig struct {
3939
ToolTimeout *int `json:"toolTimeout,omitempty"`
4040
KeepaliveInterval *int `json:"keepaliveInterval,omitempty"`
4141
PayloadDir string `json:"payloadDir,omitempty"`
42+
PayloadPathPrefix *string `json:"payloadPathPrefix,omitempty"`
4243
PayloadSizeThreshold *int `json:"payloadSizeThreshold,omitempty"`
4344
TrustedBots []string `json:"trustedBots,omitempty"`
4445
OpenTelemetry *StdinOpenTelemetryConfig `json:"opentelemetry,omitempty"`
@@ -129,17 +130,19 @@ type StdinServerConfig struct {
129130

130131
// ConnectTimeout is the per-transport timeout (in seconds) for connecting to HTTP backends.
131132
// Only applies to HTTP server types. Default: 30 seconds.
132-
ConnectTimeout *int `json:"connect_timeout,omitempty"`
133+
ConnectTimeout *int `json:"connectTimeout,omitempty"`
133134

134135
// ToolTimeout is the per-server maximum time (seconds) to wait for a single tool invocation.
135136
// When set to a positive value, this overrides the global gateway.toolTimeout for calls to
136137
// this server only. Minimum: 10. Omit the field (or set to 0) to fall back to the global
137138
// gateway.toolTimeout (or MCP_GATEWAY_TOOL_TIMEOUT env fallback).
138-
ToolTimeout *int `json:"tool_timeout,omitempty"`
139+
ToolTimeout *int `json:"toolTimeout,omitempty"`
139140

140141
// AdditionalProperties stores any extra fields for custom server types
141142
// This allows custom schemas to define their own fields beyond the standard ones
142143
AdditionalProperties map[string]interface{} `json:"-"`
144+
145+
toolTimeoutFieldName string `json:"-"`
143146
}
144147

145148
// UnmarshalJSON implements custom JSON unmarshaling to capture additional properties
@@ -157,6 +160,25 @@ func (s *StdinServerConfig) UnmarshalJSON(data []byte) error {
157160
return err
158161
}
159162

163+
var rawFields map[string]json.RawMessage
164+
if err := json.Unmarshal(data, &rawFields); err != nil {
165+
return err
166+
}
167+
168+
s.toolTimeoutFieldName = "toolTimeout"
169+
if _, ok := rawFields["toolTimeout"]; !ok {
170+
if _, legacyOK := rawFields["tool_timeout"]; legacyOK {
171+
s.toolTimeoutFieldName = "tool_timeout"
172+
}
173+
}
174+
175+
if err := assignLegacyIntAlias(rawFields, "connect_timeout", &s.ConnectTimeout); err != nil {
176+
return err
177+
}
178+
if err := assignLegacyIntAlias(rawFields, "tool_timeout", &s.ToolTimeout); err != nil {
179+
return err
180+
}
181+
160182
// Now unmarshal into a map to capture all fields
161183
var allFields map[string]interface{}
162184
if err := json.Unmarshal(data, &allFields); err != nil {
@@ -180,7 +202,9 @@ func (s *StdinServerConfig) UnmarshalJSON(data []byte) error {
180202
"guard-policies": true,
181203
"guard": true,
182204
"auth": true,
205+
"connectTimeout": true,
183206
"connect_timeout": true,
207+
"toolTimeout": true,
184208
"tool_timeout": true,
185209
}
186210

@@ -195,6 +219,30 @@ func (s *StdinServerConfig) UnmarshalJSON(data []byte) error {
195219
return nil
196220
}
197221

222+
func (s *StdinServerConfig) toolTimeoutField() string {
223+
if s.toolTimeoutFieldName != "" {
224+
return s.toolTimeoutFieldName
225+
}
226+
return "toolTimeout"
227+
}
228+
229+
func assignLegacyIntAlias(rawFields map[string]json.RawMessage, alias string, target **int) error {
230+
if *target != nil {
231+
return nil
232+
}
233+
raw, ok := rawFields[alias]
234+
if !ok {
235+
return nil
236+
}
237+
238+
var value int
239+
if err := json.Unmarshal(raw, &value); err != nil {
240+
return fmt.Errorf("invalid %s value: %w", alias, err)
241+
}
242+
*target = &value
243+
return nil
244+
}
245+
198246
// intPtrOrDefault returns the value of the int pointer if not nil, otherwise returns the default value.
199247
// This helper reduces code duplication when handling optional integer fields with defaults.
200248
func intPtrOrDefault(ptr *int, defaultValue int) int {
@@ -333,6 +381,9 @@ func convertStdinConfig(stdinCfg *StdinConfig) (*Config, error) {
333381
if stdinCfg.Gateway.PayloadDir != "" {
334382
cfg.Gateway.PayloadDir = stdinCfg.Gateway.PayloadDir
335383
}
384+
if stdinCfg.Gateway.PayloadPathPrefix != nil {
385+
cfg.Gateway.PayloadPathPrefix = *stdinCfg.Gateway.PayloadPathPrefix
386+
}
336387
if stdinCfg.Gateway.PayloadSizeThreshold != nil {
337388
cfg.Gateway.PayloadSizeThreshold = *stdinCfg.Gateway.PayloadSizeThreshold
338389
}
@@ -518,7 +569,7 @@ func buildStdioServerConfig(name string, server *StdinServerConfig) *ServerConfi
518569
logStdin.Printf("Server %q: configured stdio container=%s, env_vars=%d, mounts=%d", name, server.Container, len(server.Env), len(server.Mounts))
519570
logConfig.Printf("Configured stdio MCP server: name=%s, container=%s", name, server.Container)
520571

521-
return &ServerConfig{
572+
serverCfg := &ServerConfig{
522573
Type: "stdio",
523574
Command: "docker",
524575
Args: args,
@@ -529,6 +580,10 @@ func buildStdioServerConfig(name string, server *StdinServerConfig) *ServerConfi
529580
GuardPolicies: server.GuardPolicies,
530581
Guard: server.Guard,
531582
}
583+
if server.ToolTimeout != nil {
584+
serverCfg.ToolTimeout = *server.ToolTimeout
585+
}
586+
return serverCfg
532587
}
533588

534589
// normalizeLocalType normalizes "local" type to "stdio" for backward compatibility.

0 commit comments

Comments
 (0)