Skip to content

Commit a0084a2

Browse files
committed
feat(upstream): configurable per-server MCP initialize timeout
Add init_timeout (global default + per-server override) controlling the deadline for an upstream's MCP initialize handshake. Un-isolated stdio servers previously inherited the caller's ~30s context and were killed mid-startup during legitimate first-run warmup (cache/index build); with docker run --rm the partial work was discarded and retries looped forever (GH #760). - config: InitTimeout *Duration on Config and ServerConfig; ResolveInitTimeout (per-server > global > 30s default; <=0 maps to default, never disabled); bounds {0} u [1s,30m] in Validate(). - upstream/manager: resolveConnectTimeout applied at AddServer, ConnectAll (wrap ALL connects, not just Docker), and the post-OAuth retry; Docker isolation keeps a 3m floor, a larger override still wins. - Plumb init_timeout through the upstream_servers MCP tool (add/patch), REST AddServerRequest allowlist + GET serialization, config merge/copy, storage UpstreamRecord, contracts.Server, and mcpproxy upstream patch --init-timeout. - Docs (configuration.md) + regenerated oas/swagger.yaml. Related #760
1 parent dc9ebf6 commit a0084a2

24 files changed

Lines changed: 601 additions & 16 deletions

cmd/mcpproxy/upstream_cmd.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ Examples:
286286
upstreamPatchHeaderRemove []string
287287
upstreamPatchEnvs []string
288288
upstreamPatchEnvRemove []string
289+
upstreamPatchInitTimeout string
289290

290291
// Import command flags
291292
upstreamImportServer string
@@ -361,6 +362,7 @@ func init() {
361362
upstreamPatchCmd.Flags().StringArrayVar(&upstreamPatchHeaderRemove, "header-remove", nil, "HTTP header name to delete (repeatable)")
362363
upstreamPatchCmd.Flags().StringArrayVar(&upstreamPatchEnvs, "env", nil, "Environment variable to upsert in KEY=value format (repeatable)")
363364
upstreamPatchCmd.Flags().StringArrayVar(&upstreamPatchEnvRemove, "env-remove", nil, "Environment variable name to delete (repeatable)")
365+
upstreamPatchCmd.Flags().StringVar(&upstreamPatchInitTimeout, "init-timeout", "", "MCP initialize handshake deadline as a duration (e.g. '120s', '3m'); raise for upstreams that warm up before responding to initialize")
364366

365367
// Inspect command flags
366368
upstreamInspectCmd.Flags().StringVar(&upstreamInspectTool, "tool", "", "Show details for a specific tool")
@@ -2075,9 +2077,18 @@ func runUpstreamPatch(_ *cobra.Command, args []string) error {
20752077
return fmt.Errorf("server name is required")
20762078
}
20772079

2080+
initTimeout := strings.TrimSpace(upstreamPatchInitTimeout)
20782081
if len(upstreamPatchHeaders) == 0 && len(upstreamPatchHeaderRemove) == 0 &&
2079-
len(upstreamPatchEnvs) == 0 && len(upstreamPatchEnvRemove) == 0 {
2080-
return fmt.Errorf("at least one of --header / --header-remove / --env / --env-remove must be specified")
2082+
len(upstreamPatchEnvs) == 0 && len(upstreamPatchEnvRemove) == 0 && initTimeout == "" {
2083+
return fmt.Errorf("at least one of --header / --header-remove / --env / --env-remove / --init-timeout must be specified")
2084+
}
2085+
2086+
// Validate --init-timeout locally so we fail fast with a clear message
2087+
// before hitting the daemon (the backend re-validates the bounds).
2088+
if initTimeout != "" {
2089+
if _, perr := time.ParseDuration(initTimeout); perr != nil {
2090+
return fmt.Errorf("invalid --init-timeout %q: %v (expected a duration like '120s' or '3m')", initTimeout, perr)
2091+
}
20812092
}
20822093

20832094
headers := map[string]*string{}
@@ -2135,6 +2146,9 @@ func runUpstreamPatch(_ *cobra.Command, args []string) error {
21352146
if len(envs) > 0 {
21362147
body["env"] = envs
21372148
}
2149+
if initTimeout != "" {
2150+
body["init_timeout"] = initTimeout
2151+
}
21382152

21392153
bodyBytes, err := json.Marshal(body)
21402154
if err != nil {
@@ -2176,6 +2190,9 @@ func runUpstreamPatch(_ *cobra.Command, args []string) error {
21762190
if n := len(upstreamPatchEnvRemove); n > 0 {
21772191
parts = append(parts, fmt.Sprintf("%d env var(s) removed", n))
21782192
}
2193+
if initTimeout != "" {
2194+
parts = append(parts, fmt.Sprintf("init_timeout=%s", initTimeout))
2195+
}
21792196
if len(parts) > 0 {
21802197
fmt.Printf(": %s", strings.Join(parts, ", "))
21812198
}

docs/configuration.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ MCPProxy looks for configuration in these locations (in order):
8686
{
8787
"tools_limit": 15,
8888
"tool_response_limit": 20000,
89-
"call_tool_timeout": "2m"
89+
"call_tool_timeout": "2m",
90+
"init_timeout": "30s"
9091
}
9192
```
9293

@@ -95,6 +96,7 @@ MCPProxy looks for configuration in these locations (in order):
9596
| `tools_limit` | integer | `15` | Maximum number of tools to return per request (1-1000) |
9697
| `tool_response_limit` | integer | `20000` | Maximum characters in tool responses (0 = unlimited) |
9798
| `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 |
99+
| `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. |
98100

99101
### Discovery & Health Checks
100102

@@ -208,6 +210,7 @@ the proxy.
208210
| `launcher_wait_timeout` | duration | No | When `command` is set together with an HTTP/SSE `url`, how long mcpproxy waits for that URL to become reachable after spawning the child (e.g. `"15s"`, default `"30s"`) |
209211
| `health_check_interval` | duration | No | Per-server override for the global [`health_check_interval`](#discovery--health-checks). `"0s"` disables the liveness probe for this server only. Range: `5s``1h`. Omit to inherit the global value. |
210212
| `tool_discovery_interval` | duration | No | Per-server override for the global [`tool_discovery_interval`](#discovery--health-checks). Overrides the global/default cadence for this server only; `"0s"` disables the periodic tool-discovery sweep for this server (connect-time and reactive `list_changed` discovery still run). Range: `30s``24h`. Omit to inherit the global value. |
213+
| `init_timeout` | duration | No | Per-server override for the global [`init_timeout`](#search--tool-limits) — the MCP `initialize` handshake deadline. Raise it for an upstream that warms up (caches/indexes data) before responding to `initialize` (e.g. `"120s"`, `"3m"`); without it such a server is killed mid-startup and, with `docker run --rm`, retries forever. Range: `1s``30m`. Omit to inherit the global value (30s default). Settable via the `upstream_servers` tool and `mcpproxy upstream patch --init-timeout`. |
211214
| `oauth` | object | No | OAuth configuration (see [OAuth Configuration](#oauth-configuration)) |
212215
| `isolation` | object | No | Per-server Docker isolation settings (see [Docker Isolation](#docker-isolation)) |
213216
| `enabled` | boolean | No | Enable/disable server (default: `true`) |

internal/config/config.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ func (d Duration) Duration() time.Duration {
5858
const (
5959
defaultHealthCheckInterval = 30 * time.Second
6060
defaultToolDiscoveryInterval = 5 * time.Minute
61+
// defaultInitTimeout is the deadline applied to a stdio/HTTP upstream's MCP
62+
// `initialize` handshake when no per-server or global override is set
63+
// (MCP-3322 / GH #760). It preserves the historical ~30s behaviour.
64+
defaultInitTimeout = 30 * time.Second
6165
)
6266

6367
// resolveInterval applies the per-server → global → default precedence for an
@@ -97,6 +101,23 @@ func (c *Config) ResolveToolDiscoveryInterval(sc *ServerConfig) time.Duration {
97101
return resolveInterval(server, c.ToolDiscoveryInterval, defaultToolDiscoveryInterval)
98102
}
99103

104+
// ResolveInitTimeout resolves the effective MCP `initialize` handshake deadline
105+
// for a server: per-server override → global → 30s default (MCP-3322 / GH #760).
106+
// Unlike the discovery intervals, a resolved value <= 0 maps to the default
107+
// rather than "disabled" — a zero/negative connect deadline would let a stuck
108+
// upstream hang the connect path forever, so we always keep a real ceiling.
109+
func (c *Config) ResolveInitTimeout(sc *ServerConfig) time.Duration {
110+
var server *Duration
111+
if sc != nil {
112+
server = sc.InitTimeout
113+
}
114+
resolved := resolveInterval(server, c.InitTimeout, defaultInitTimeout)
115+
if resolved <= 0 {
116+
return defaultInitTimeout
117+
}
118+
return resolved
119+
}
120+
100121
// Config represents the main configuration structure
101122
type Config struct {
102123
Listen string `json:"listen" mapstructure:"listen"`
@@ -127,6 +148,15 @@ type Config struct {
127148
HealthCheckInterval *Duration `json:"health_check_interval,omitempty" mapstructure:"health-check-interval" swaggertype:"string"`
128149
ToolDiscoveryInterval *Duration `json:"tool_discovery_interval,omitempty" mapstructure:"tool-discovery-interval" swaggertype:"string"`
129150

151+
// InitTimeout is the global default deadline for an upstream's MCP
152+
// `initialize` handshake (MCP-3322 / GH #760). *Duration tri-state: nil =
153+
// inherit the built-in 30s default; a positive value = that deadline. A
154+
// per-server InitTimeout overrides this. Resolved by ResolveInitTimeout;
155+
// validated to {0} ∪ [1s, 30m] in Validate(). Servers doing legitimate
156+
// first-run warmup (cache/index build) before answering `initialize` can
157+
// raise this so they are not killed mid-startup.
158+
InitTimeout *Duration `json:"init_timeout,omitempty" mapstructure:"init-timeout" swaggertype:"string"`
159+
130160
// Environment configuration for secure variable filtering
131161
Environment *secureenv.EnvConfig `json:"environment,omitempty" mapstructure:"environment"`
132162

@@ -367,6 +397,14 @@ type ServerConfig struct {
367397
HealthCheckInterval *Duration `json:"health_check_interval,omitempty" mapstructure:"health_check_interval" swaggertype:"string"`
368398
ToolDiscoveryInterval *Duration `json:"tool_discovery_interval,omitempty" mapstructure:"tool_discovery_interval" swaggertype:"string"`
369399

400+
// InitTimeout overrides the global init_timeout for this server's MCP
401+
// `initialize` handshake deadline (MCP-3322 / GH #760). *Duration tri-state:
402+
// nil = inherit the global value (or 30s default), positive = that deadline.
403+
// Resolved by Config.ResolveInitTimeout; validated to {0} ∪ [1s, 30m]. Raise
404+
// this for upstreams that do legitimate first-run warmup (e.g. caching many
405+
// channels/users) before responding to `initialize`.
406+
InitTimeout *Duration `json:"init_timeout,omitempty" mapstructure:"init_timeout" swaggertype:"string"`
407+
370408
EnabledTools []string `json:"enabled_tools,omitempty" mapstructure:"enabled_tools"` // Allowlist: only these tools are exposed; mutually exclusive with disabled_tools
371409
DisabledTools []string `json:"disabled_tools,omitempty" mapstructure:"disabled_tools"` // Denylist: these tools are hidden; mutually exclusive with enabled_tools
372410

@@ -1455,6 +1493,9 @@ func (c *Config) ValidateDetailed() []ValidationError {
14551493
}
14561494

14571495
// Validate global discovery/health-check intervals (spec 074, FR-008).
1496+
if e := validateIntervalBound("init_timeout", c.InitTimeout, time.Second, 30*time.Minute); e != nil {
1497+
errors = append(errors, *e)
1498+
}
14581499
if e := validateIntervalBound("health_check_interval", c.HealthCheckInterval, 5*time.Second, time.Hour); e != nil {
14591500
errors = append(errors, *e)
14601501
}
@@ -1578,6 +1619,10 @@ func (c *Config) ValidateDetailed() []ValidationError {
15781619
if e := validateIntervalBound(fieldPrefix+".tool_discovery_interval", server.ToolDiscoveryInterval, 30*time.Second, 24*time.Hour); e != nil {
15791620
errors = append(errors, *e)
15801621
}
1622+
// MCP-3322: per-server MCP `initialize` handshake deadline override.
1623+
if e := validateIntervalBound(fieldPrefix+".init_timeout", server.InitTimeout, time.Second, 30*time.Minute); e != nil {
1624+
errors = append(errors, *e)
1625+
}
15811626
}
15821627

15831628
// Validate DataDir exists (if specified and not empty).
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package config
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
// TestResolveInitTimeout covers the per-server → global → default precedence
9+
// (MCP-3322 / GH #760). Unlike the discovery intervals, a resolved value <= 0
10+
// (including a pointer-to-zero override) maps to the 30s default rather than
11+
// "disabled" — we never want a hang-forever connect deadline.
12+
func TestResolveInitTimeout(t *testing.T) {
13+
cases := []struct {
14+
name string
15+
global *Duration
16+
server *Duration
17+
want time.Duration
18+
}{
19+
{"both unset → default", nil, nil, 30 * time.Second},
20+
{"global set, server unset → global", durPtr(120 * time.Second), nil, 120 * time.Second},
21+
{"server override wins", durPtr(120 * time.Second), durPtr(90 * time.Second), 90 * time.Second},
22+
{"server override wins over default when global unset", nil, durPtr(45 * time.Second), 45 * time.Second},
23+
{"global zero → default (never disabled)", durPtr(0), nil, 30 * time.Second},
24+
{"server zero → default (never disabled)", durPtr(120 * time.Second), durPtr(0), 30 * time.Second},
25+
{"server negative → default", nil, durPtr(-5 * time.Second), 30 * time.Second},
26+
}
27+
for _, tc := range cases {
28+
t.Run(tc.name, func(t *testing.T) {
29+
c := &Config{InitTimeout: tc.global}
30+
sc := &ServerConfig{InitTimeout: tc.server}
31+
got := c.ResolveInitTimeout(sc)
32+
if got != tc.want {
33+
t.Errorf("ResolveInitTimeout = %v, want %v", got, tc.want)
34+
}
35+
})
36+
}
37+
38+
// A nil server config must still resolve via global → default.
39+
c := &Config{InitTimeout: durPtr(120 * time.Second)}
40+
if got := c.ResolveInitTimeout(nil); got != 120*time.Second {
41+
t.Errorf("nil server: got %v, want 120s", got)
42+
}
43+
}
44+
45+
// TestValidateInitTimeoutBounds covers the bounds contract: 0s accepted (means
46+
// "use default"), in-range [1s, 30m] accepted, out-of-range rejected — for both
47+
// the global and per-server pointers.
48+
func TestValidateInitTimeoutBounds(t *testing.T) {
49+
cases := []struct {
50+
name string
51+
mutate func(*Config)
52+
wantError bool
53+
}{
54+
{"0s accepted (default)", func(c *Config) { c.InitTimeout = durPtr(0) }, false},
55+
{"1s accepted (min)", func(c *Config) { c.InitTimeout = durPtr(time.Second) }, false},
56+
{"30m accepted (max)", func(c *Config) { c.InitTimeout = durPtr(30 * time.Minute) }, false},
57+
{"500ms rejected (below min)", func(c *Config) { c.InitTimeout = durPtr(500 * time.Millisecond) }, true},
58+
{"31m rejected (above max)", func(c *Config) { c.InitTimeout = durPtr(31 * time.Minute) }, true},
59+
{"negative rejected", func(c *Config) { c.InitTimeout = durPtr(-1 * time.Second) }, true},
60+
{"per-server 500ms rejected", func(c *Config) {
61+
c.Servers = []*ServerConfig{{Name: "s", InitTimeout: durPtr(500 * time.Millisecond)}}
62+
}, true},
63+
{"per-server 120s accepted", func(c *Config) {
64+
c.Servers = []*ServerConfig{{Name: "s", InitTimeout: durPtr(120 * time.Second)}}
65+
}, false},
66+
}
67+
for _, tc := range cases {
68+
t.Run(tc.name, func(t *testing.T) {
69+
c := DefaultConfig()
70+
tc.mutate(c)
71+
errs := c.ValidateDetailed()
72+
hasErr := false
73+
for _, e := range errs {
74+
if containsAny(e.Field, "init_timeout") {
75+
hasErr = true
76+
}
77+
}
78+
if hasErr != tc.wantError {
79+
t.Errorf("validation init_timeout error = %v, want %v (errors: %+v)", hasErr, tc.wantError, errs)
80+
}
81+
})
82+
}
83+
}

internal/config/merge.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,17 @@ func MergeServerConfig(base, patch *ServerConfig, opts MergeOptions) (*ServerCon
319319
merged.OAuth = newOAuth
320320
}
321321

322+
// InitTimeout (MCP-3322): *Duration tri-state. A non-nil pointer in the
323+
// patch sets/replaces the per-server override; nil leaves the base value
324+
// (already carried by CopyServerConfig) untouched.
325+
if patch.InitTimeout != nil {
326+
it := *patch.InitTimeout
327+
if diff != nil && (base.InitTimeout == nil || *base.InitTimeout != it) {
328+
diff.Modified["init_timeout"] = FieldChange{Path: "init_timeout", From: base.InitTimeout, To: patch.InitTimeout}
329+
}
330+
merged.InitTimeout = &it
331+
}
332+
322333
// Always update the Updated timestamp
323334
merged.Updated = time.Now()
324335

@@ -588,6 +599,10 @@ func CopyServerConfig(src *ServerConfig) *ServerConfig {
588599
td := *src.ToolDiscoveryInterval
589600
dst.ToolDiscoveryInterval = &td
590601
}
602+
if src.InitTimeout != nil {
603+
it := *src.InitTimeout
604+
dst.InitTimeout = &it
605+
}
591606

592607
// Copy the per-upstream auth-broker block by value (spec 074, server edition).
593608
// In the personal edition AuthBrokerConfig is an empty stub struct, so this is

internal/config/merge_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,19 +1006,25 @@ func TestCopyServerConfig_AllFields(t *testing.T) {
10061006
func TestCopyServerConfig_PreservesAllowlistAndOverrides(t *testing.T) {
10071007
hc := Duration(30 * time.Second)
10081008
td := Duration(5 * time.Minute)
1009+
it := Duration(120 * time.Second)
10091010
src := &ServerConfig{
10101011
Name: "srv",
10111012
ReconnectOnUse: true,
10121013
EnabledTools: []string{"a", "b"},
10131014
DisabledTools: []string{"c"},
10141015
HealthCheckInterval: &hc,
10151016
ToolDiscoveryInterval: &td,
1017+
InitTimeout: &it,
10161018
SourceRegistryID: "reg-1",
10171019
SourceRegistryProvenance: "official",
10181020
}
10191021

10201022
dst := CopyServerConfig(src)
10211023

1024+
if dst.InitTimeout == nil || *dst.InitTimeout != it {
1025+
t.Errorf("InitTimeout: got %v, want %v", dst.InitTimeout, it)
1026+
}
1027+
10221028
if !dst.ReconnectOnUse {
10231029
t.Error("ReconnectOnUse was dropped")
10241030
}
@@ -1052,6 +1058,57 @@ func TestCopyServerConfig_PreservesAllowlistAndOverrides(t *testing.T) {
10521058
}
10531059
}
10541060

1061+
// TestMergeServerConfig_InitTimeout covers MCP-3322: a patch carrying
1062+
// init_timeout sets/replaces the per-server override (and records a diff), while
1063+
// a patch that omits it preserves the existing value.
1064+
func TestMergeServerConfig_InitTimeout(t *testing.T) {
1065+
t.Run("patch sets init_timeout from unset", func(t *testing.T) {
1066+
base := &ServerConfig{Name: "srv", Enabled: true}
1067+
it := Duration(120 * time.Second)
1068+
patch := &ServerConfig{InitTimeout: &it}
1069+
1070+
merged, diff, err := MergeServerConfig(base, patch, DefaultMergeOptions())
1071+
if err != nil {
1072+
t.Fatalf("merge: %v", err)
1073+
}
1074+
if merged.InitTimeout == nil || *merged.InitTimeout != it {
1075+
t.Errorf("InitTimeout: got %v, want %v", merged.InitTimeout, it)
1076+
}
1077+
if diff == nil || diff.Modified["init_timeout"].Path != "init_timeout" {
1078+
t.Errorf("expected init_timeout in diff, got %+v", diff)
1079+
}
1080+
})
1081+
1082+
t.Run("unrelated patch preserves init_timeout", func(t *testing.T) {
1083+
it := Duration(120 * time.Second)
1084+
base := &ServerConfig{Name: "srv", Enabled: true, InitTimeout: &it}
1085+
patch := &ServerConfig{Enabled: true, URL: "http://example.com/mcp"}
1086+
1087+
merged, _, err := MergeServerConfig(base, patch, DefaultMergeOptions())
1088+
if err != nil {
1089+
t.Fatalf("merge: %v", err)
1090+
}
1091+
if merged.InitTimeout == nil || *merged.InitTimeout != it {
1092+
t.Errorf("InitTimeout was wiped on unrelated patch: got %v, want %v", merged.InitTimeout, it)
1093+
}
1094+
})
1095+
1096+
t.Run("patch replaces existing init_timeout", func(t *testing.T) {
1097+
old := Duration(120 * time.Second)
1098+
base := &ServerConfig{Name: "srv", Enabled: true, InitTimeout: &old}
1099+
newVal := Duration(45 * time.Second)
1100+
patch := &ServerConfig{InitTimeout: &newVal}
1101+
1102+
merged, _, err := MergeServerConfig(base, patch, DefaultMergeOptions())
1103+
if err != nil {
1104+
t.Fatalf("merge: %v", err)
1105+
}
1106+
if merged.InitTimeout == nil || *merged.InitTimeout != newVal {
1107+
t.Errorf("InitTimeout: got %v, want %v", merged.InitTimeout, newVal)
1108+
}
1109+
})
1110+
}
1111+
10551112
// TestMergeServerConfig_PreservesDisabledToolsOnUnrelatedPatch is the
10561113
// end-to-end regression: patching an unrelated field on a server that has a
10571114
// disabled_tools denylist must not wipe the denylist (it previously did,

internal/contracts/converters.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ func ConvertServerConfig(cfg *config.ServerConfig, status string, connected bool
3636
// MCP-2940: surface the per-server auto-approve intent (tri-state *bool)
3737
// so the Web UI toggle reflects the persisted value.
3838
AutoApproveToolChanges: cfg.AutoApproveToolChanges,
39+
// MCP-3322: surface the per-server init_timeout override so callers can
40+
// read back a configured handshake deadline.
41+
InitTimeout: cfg.InitTimeout,
3942
}
4043

4144
// Convert OAuth config if present
@@ -187,6 +190,14 @@ func ConvertGenericServersToTyped(genericServers []map[string]interface{}) []Ser
187190
v := autoApprove
188191
server.AutoApproveToolChanges = &v
189192
}
193+
// MCP-3322: init_timeout serializes as a duration string (e.g. "120s").
194+
// Parse it back into a *config.Duration so the GET payload round-trips.
195+
if initTimeout, ok := generic["init_timeout"].(string); ok && initTimeout != "" {
196+
if d, err := time.ParseDuration(initTimeout); err == nil {
197+
v := config.Duration(d)
198+
server.InitTimeout = &v
199+
}
200+
}
190201
if connected, ok := generic["connected"].(bool); ok {
191202
server.Connected = connected
192203
}

0 commit comments

Comments
 (0)