feat: add per-server tool_timeout to override global gateway timeout#5206
feat: add per-server tool_timeout to override global gateway timeout#5206
Conversation
Adds a new `tool_timeout` field to server configurations that allows reusable shared workflows to set an appropriate tool invocation timeout for their specific MCP server without requiring consumers to configure the global `MCP_GATEWAY_TOOL_TIMEOUT` env var. Changes: - ServerConfig.ToolTimeout: new per-server tool timeout field (TOML + JSON) - StdinServerConfig.ToolTimeout: JSON stdin support for HTTP/stdio servers - JSON schema: tool_timeout added to httpServerConfig and stdioServerConfig - callBackendTool: per-server ToolTimeout takes precedence over global - validateStandardServerConfig: validates per-server tool_timeout >= 10 - docs/CONFIGURATION.md: documents the new tool_timeout field - Tests: validation, config parsing, and server behavior tests Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/60af4feb-95e7-48d7-b1e8-8729f32389a4 Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a per-server tool_timeout configuration option so individual MCP backends can override the global gateway tool invocation timeout, improving reusable workflow ergonomics for long-running servers.
Changes:
- Introduces
ServerConfig.ToolTimeout/tool_timeout(TOML + stdin JSON) and wires it through stdin parsing + schema validation. - Updates
UnifiedServer.callBackendToolto prefer per-server timeout overgateway.ToolTimeoutwhen set. - Adds validation/tests and documentation for the new field.
Show a summary per file
| File | Description |
|---|---|
| internal/server/unified.go | Applies per-server timeout precedence when creating the tool-call context deadline. |
| internal/server/call_backend_tool_test.go | Adds tests for per-server timeout override and global fallback behavior. |
| internal/config/validation.go | Validates per-server tool_timeout for stdin configs. |
| internal/config/validation_test.go | Adds unit tests covering per-server tool_timeout validation cases. |
| internal/config/schema/mcp-gateway-config.schema.json | Extends stdin JSON schema to accept tool_timeout for stdio/http servers. |
| internal/config/config_test.go | Adds stdin loading tests verifying tool_timeout parsing and minimum enforcement. |
| internal/config/config_stdin.go | Adds tool_timeout to stdin server config struct and conversion to internal ServerConfig. |
| internal/config/config_core.go | Adds ToolTimeout field to core ServerConfig (TOML/JSON file + internal representation). |
| docs/CONFIGURATION.md | Documents tool_timeout and its relationship to the global gateway tool timeout. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 9/9 changed files
- Comments generated: 6
| if serverCfg, ok := us.cfg.Servers[serverID]; ok && serverCfg.ToolTimeout > 0 { | ||
| toolTimeout = serverCfg.ToolTimeout | ||
| logUnified.Printf("callBackendTool: using per-server tool_timeout=%d for serverID=%s", toolTimeout, serverID) | ||
| } else if us.cfg.Gateway != nil && us.cfg.Gateway.ToolTimeout > 0 { |
| // Validate per-server tool_timeout if provided | ||
| if server.ToolTimeout != nil { | ||
| if err := rules.TimeoutMinimum(*server.ToolTimeout, ToolTimeoutMin, "tool_timeout", jsonPath+".tool_timeout"); err != nil { | ||
| logValidateServerFailed(name, fmt.Sprintf("tool_timeout %d is below minimum %d", *server.ToolTimeout, ToolTimeoutMin)) | ||
| return err | ||
| } | ||
| } |
|
|
||
| // ToolTimeout is the per-server maximum time (seconds) to wait for a single tool invocation. | ||
| // When set, this overrides the global gateway.toolTimeout for calls to this server only. | ||
| // Minimum: 10. When 0 or omitted, the global gateway.toolTimeout is used. |
| }, | ||
| "tool_timeout": { | ||
| "type": "integer", | ||
| "description": "Per-server tool invocation timeout in seconds. When set, overrides the global gateway.toolTimeout for tool calls to this server only. Minimum: 10.", | ||
| "minimum": 10 | ||
| } |
|
|
||
| - **`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`. | ||
|
|
||
| - **`tool_timeout`** (optional): Per-server tool invocation timeout in seconds. When set, overrides the global `gateway.toolTimeout` 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`. When omitted, the global `gateway.toolTimeout` (or its `MCP_GATEWAY_TOOL_TIMEOUT` env fallback) is used. |
| // Delay longer than per-server timeout (1s), but shorter than global (30s) | ||
| time.Sleep(3 * time.Second) |
|
@copilot address the review feedback |
- Add nil guard for serverCfg in callBackendTool (unified.go) - Allow tool_timeout=0 to mean "unset/fallback to global"; update validation, schema (oneOf: 0 or >=10), comments, and test expectations accordingly - Clarify global timeout field names per format in docs (toolTimeout JSON / tool_timeout TOML) - Replace fixed 3s sleep in test handler with context-aware select to avoid slow test execution Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/86ea6ba6-24d0-47b7-8ec2-79419e538001 Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
All review feedback addressed in 2ad7bac:
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot fix the lint errors |
…-gateway-timeout Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
… error Merged latest main (which introduced get_server_status_test.go) and renamed the second declaration of TestGetServerStatus_EmptyConfig to TestGetServerStatus_NilServersField to reflect its distinct scenario (nil Servers field vs empty Servers map). Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/41c2d427-761b-43b9-9d47-9be47955824f Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Fixed in 43d605c. The lint failure was caused by |
Reusable shared workflows wrapping long-running HTTP MCP servers (e.g. Repo Mind Light) have no way to set the gateway's tool invocation timeout — consumers must know to configure
MCP_GATEWAY_TOOL_TIMEOUTinsandbox.mcp.env, breaking the reusable component contract.Changes
ServerConfig.ToolTimeout— new per-server field (TOML:tool_timeout, JSON stdin:tool_timeout); minimum 10s; zero/omitted means fall back togateway.toolTimeoutcallBackendTool— per-serverToolTimeouttakes precedence overgateway.ToolTimeoutwhen settool_timeoutadded tostdioServerConfigandhttpServerConfigdefinitions so stdin validation accepts the fieldvalidateStandardServerConfigenforces minimum of 10 for per-servertool_timeoutCONFIGURATION.mddocuments the field and its relationship to the global timeoutExample
A reusable shared workflow can now declare its own timeout directly on the server:
{ "mcpServers": { "repo-mind": { "type": "http", "url": "http://127.0.0.1:8000/mcp", "tool_timeout": 600 } }, "gateway": { "port": 3000, "domain": "localhost", "apiKey": "${API_KEY}" } }Consumers get a sane 600s timeout without needing to know about
MCP_GATEWAY_TOOL_TIMEOUT. They can still override at the consumer level if needed.Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
example.com/tmp/go-build2847888803/b509/launcher.test /tmp/go-build2847888803/b509/launcher.test -test.testlogfile=/tmp/go-build2847888803/b509/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 997891/b463/_pkg_.a -trimpath ache/go/1.25.9/x64/pkg/tool/linux_amd64/vet -p ny/timefmt-go -lang=go1.25 ache/go/1.25.9/x-buildtags(dns block)/tmp/go-build1669811436/b513/launcher.test /tmp/go-build1669811436/b513/launcher.test -test.testlogfile=/tmp/go-build1669811436/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s dock�� -stringintconv -tests 64/pkg/tool/linux_amd64/link _.a 997891/b150/ x_amd64/vet 64/pkg/tool/linux_amd64/link --no�� test.test x_amd64/vet x_amd64/vet 1.80.0/channelz/docker 997891/b150/ 64/pkg/tool/linu-nilfunc x_amd64/vet(dns block)invalid-host-that-does-not-exist-12345.com/tmp/go-build871958364/b001/config.test /tmp/go-build871958364/b001/config.test -test.paniconexit0 -test.timeout=2m0s -test.count=1 rtcf�� 1024825-9d38bb4040a9/status/status.pb.go 64/src/internal/google.golang.org/grpc/internal/channelz x86_64/as(dns block)/tmp/go-build2847888803/b491/config.test /tmp/go-build2847888803/b491/config.test -test.testlogfile=/tmp/go-build2847888803/b491/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true msSmPlS8N cfg x_amd64/vet . -imultiarch x86_64-linux-gnu--noprofile x_amd64/vet 9978�� om/spf13/cobra@v1.10.2/active_he-errorsas cfg x_amd64/vet --gdwarf-5 b/gh-aw-mcpg/intphp8.3 -o x_amd64/vet(dns block)/tmp/go-build1669811436/b495/config.test /tmp/go-build1669811436/b495/config.test -test.testlogfile=/tmp/go-build1669811436/b495/testlog.txt -test.paniconexit0 -test.timeout=10m0s -ato�� -bool -buildtags x_amd64/vet -errorsas -ifaceassert -nilfunc x_amd64/vet run test -tests ache/go/1.25.9/x64/pkg/tool/linux_amd64/compile _.a aw-mcpg/internal/tmp/go-build1282575160/b425/vet.cfg x_amd64/vet ache/go/1.25.9/x64/pkg/tool/linux_amd64/compile(dns block)nonexistent.local/tmp/go-build2847888803/b509/launcher.test /tmp/go-build2847888803/b509/launcher.test -test.testlogfile=/tmp/go-build2847888803/b509/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 997891/b463/_pkg_.a -trimpath ache/go/1.25.9/x64/pkg/tool/linux_amd64/vet -p ny/timefmt-go -lang=go1.25 ache/go/1.25.9/x-buildtags(dns block)/tmp/go-build1669811436/b513/launcher.test /tmp/go-build1669811436/b513/launcher.test -test.testlogfile=/tmp/go-build1669811436/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s dock�� -stringintconv -tests 64/pkg/tool/linux_amd64/link _.a 997891/b150/ x_amd64/vet 64/pkg/tool/linux_amd64/link --no�� test.test x_amd64/vet x_amd64/vet 1.80.0/channelz/docker 997891/b150/ 64/pkg/tool/linu-nilfunc x_amd64/vet(dns block)slow.example.com/tmp/go-build2847888803/b509/launcher.test /tmp/go-build2847888803/b509/launcher.test -test.testlogfile=/tmp/go-build2847888803/b509/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 997891/b463/_pkg_.a -trimpath ache/go/1.25.9/x64/pkg/tool/linux_amd64/vet -p ny/timefmt-go -lang=go1.25 ache/go/1.25.9/x-buildtags(dns block)/tmp/go-build1669811436/b513/launcher.test /tmp/go-build1669811436/b513/launcher.test -test.testlogfile=/tmp/go-build1669811436/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s dock�� -stringintconv -tests 64/pkg/tool/linux_amd64/link _.a 997891/b150/ x_amd64/vet 64/pkg/tool/linux_amd64/link --no�� test.test x_amd64/vet x_amd64/vet 1.80.0/channelz/docker 997891/b150/ 64/pkg/tool/linu-nilfunc x_amd64/vet(dns block)this-host-does-not-exist-12345.com/tmp/go-build2847888803/b518/mcp.test /tmp/go-build2847888803/b518/mcp.test -test.testlogfile=/tmp/go-build2847888803/b518/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true se 958364/b060/vet.cfg ache/go/1.25.9/x64/pkg/tool/linux_amd64/vet . --gdwarf2 --64 ache/go/1.25.9/x-trimpath(dns block)/tmp/go-build3024656909/b518/mcp.test /tmp/go-build3024656909/b518/mcp.test -test.testlogfile=/tmp/go-build3024656909/b518/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true /tmp/go-build689997891/b448/_pkg_.a -trimpath docker-buildx -p go.opentelemetryinspect -lang=go1.24 docker-buildx -uns�� -unreachable=false /tmp/go-build871958364/b091/vet.-ifaceassert .cfg -c=4 -nolocalimports -importcfg ache/go/1.25.9/x64/pkg/tool/linu{{.Config.OpenStdin}}(dns block)/tmp/go-build3319390857/b518/mcp.test /tmp/go-build3319390857/b518/mcp.test -test.testlogfile=/tmp/go-build3319390857/b518/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true g_.a /tmp/go-build871958364/b023/vet.journal-or-kmsg x_amd64/vet -c=4 -nolocalimports -importcfg x_amd64/vet -uns�� hI9joKUGJ /tmp/go-build871958364/b152/vet.cfg ache/go/1.25.9/x64/pkg/tool/linux_amd64/vet en.go en.go ache/go/1.25.9/x-o ctHi6v3/br4fXcgGpaHZ4LUv69Ml(dns block)If you need me to access, download, or install something from one of these locations, you can either: