Skip to content

Commit 9c56216

Browse files
authored
fix: propagate MCPServer CRD timeout to RemoteMCPServer (#1277)
Closes #1272 Supersedes #1275 ### Summary Propagates the new `Timeout` field from the MCPServer CRD (added in [kagent-dev/kmcp#121](kagent-dev/kmcp#121)) to the generated `RemoteMCPServer` resources, fixing intermittent `"Failed to create MCP session"` errors for sidecar gateway deployments. ### Context [#1272](#1272) reported that the default 5s MCP connection timeout is too low for MCPServer CRD deployments where the sidecar gateway spawns stdio processes (via `uvx`/`npx`) with 2–8s cold starts. An initial approach ([#1275](#1275)) made the timeout configurable via Helm chart values, threading it through 6+ layers (Helm → ConfigMap → env → flag → handlers → translator). Per [maintainer feedback](#1275 (comment)), the proper fix is to add the timeout directly to the MCPServer CRD. ### Approach Instead of global controller-level configuration, the timeout is now a **per-resource field** on the MCPServer CRD with a kubebuilder default of `30s` (see [kagent-dev/kmcp#121](kagent-dev/kmcp#121)). `ConvertMCPServerToRemoteMCPServer` now propagates: - **`Timeout`** from `MCPServerSpec` → `RemoteMCPServerSpec` (defaults to 30s via CRD) - **`TerminateOnClose: true`** (matches ADK expected behavior for managed servers) ### Changes | File | Description | |------|-------------| | `go/go.mod` | Bump kmcp dependency to version with `Timeout` field (kmcp version 0.2.6) | | `go/internal/controller/translator/agent/adk_api_translator.go` | `ConvertMCPServerToRemoteMCPServer` propagates `Timeout` and sets `TerminateOnClose: true` | | `go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json` | Golden test updated | ### Backward Compatibility - Default timeout is `30s`, matching the previous hardcoded behavior from #1275. - When `MCPServer.Spec.Timeout` is explicitly set, that value takes precedence. - `RemoteMCPServer` resources created directly (not via MCPServer CRD) are unaffected — they continue using their own `Spec.Timeout` field. ### Depends on - [x] [kagent-dev/kmcp#121](kagent-dev/kmcp#121) must be merged and released first - [x] Update `go.mod` to replace the `replace` directive with the actual released version ### Testing - All existing unit tests pass - Golden test outputs regenerated - `go build ./...` and `go test ./internal/controller/...` pass cleanly Signed-off-by: skhedim <sebastien.khedim@gmail.com>
1 parent 391c73c commit 9c56216

File tree

6 files changed

+378
-9
lines changed

6 files changed

+378
-9
lines changed

go/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ require (
1414
github.com/gorilla/mux v1.8.1
1515
github.com/hashicorp/go-multierror v1.1.1
1616
github.com/jedib0t/go-pretty/v6 v6.7.8
17-
github.com/kagent-dev/kmcp v0.2.5
17+
github.com/kagent-dev/kmcp v0.2.6
1818
github.com/kagent-dev/mockllm v0.0.3
1919
github.com/modelcontextprotocol/go-sdk v1.2.0
2020
github.com/muesli/reflow v0.3.0

go/go.sum

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
165165
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
166166
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
167167
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
168-
github.com/kagent-dev/kmcp v0.2.2 h1:uvbKmo9IT6OT9RBNXYwGX0PWyxBLfAW1F9yWd5/wxaI=
169-
github.com/kagent-dev/kmcp v0.2.2/go.mod h1:g7wS/3m2wonRo/1DMwVoHxnilr/urPgV2hwV1DwkwrQ=
170-
github.com/kagent-dev/kmcp v0.2.5 h1:Em5A2vROJuR5JpMe5luSMe2vQJTwxX93AMXJm6Lg/E0=
171-
github.com/kagent-dev/kmcp v0.2.5/go.mod h1:g7wS/3m2wonRo/1DMwVoHxnilr/urPgV2hwV1DwkwrQ=
168+
github.com/kagent-dev/kmcp v0.2.6 h1:9h4JEbEK7/Joonucoide4GqGA0zJfoDxBTK+AXCkmNQ=
169+
github.com/kagent-dev/kmcp v0.2.6/go.mod h1:g7wS/3m2wonRo/1DMwVoHxnilr/urPgV2hwV1DwkwrQ=
172170
github.com/kagent-dev/mockllm v0.0.3 h1:hk6Oa/vxHoBrGqRig4GCzox8EqRQYXM4c3oFPP/k9Tg=
173171
github.com/kagent-dev/mockllm v0.0.3/go.mod h1:tDLemRsTZa1NdHaDbg3sgFk9cT1QWvMPlBtLVD6I2mA=
174172
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=

go/internal/controller/translator/agent/adk_api_translator.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"slices"
1515
"strconv"
1616
"strings"
17+
"time"
1718

1819
"github.com/kagent-dev/kagent/go/api/v1alpha2"
1920
"github.com/kagent-dev/kagent/go/internal/adk"
@@ -45,6 +46,12 @@ const (
4546
MCPServiceProtocolDefault = v1alpha2.RemoteMCPServerProtocolStreamableHttp
4647

4748
ProxyHostHeader = "x-kagent-host"
49+
50+
// DefaultMCPServerTimeout is the fallback connection timeout applied when
51+
// an MCPServer CRD resource does not have an explicit Timeout set (e.g.
52+
// objects created before the field was introduced). This value mirrors
53+
// the kubebuilder default on MCPServerSpec.Timeout in the kmcp CRD.
54+
DefaultMCPServerTimeout = 30 * time.Second
4855
)
4956

5057
// ValidationError indicates a configuration error that requires user action to fix.
@@ -1259,7 +1266,7 @@ func ConvertMCPServerToRemoteMCPServer(mcpServer *v1alpha1.MCPServer) (*v1alpha2
12591266
return nil, NewValidationError("cannot determine port for MCP server %s", mcpServer.Name)
12601267
}
12611268

1262-
return &v1alpha2.RemoteMCPServer{
1269+
remoteMCP := &v1alpha2.RemoteMCPServer{
12631270
ObjectMeta: metav1.ObjectMeta{
12641271
Name: mcpServer.Name,
12651272
Namespace: mcpServer.Namespace,
@@ -1268,7 +1275,20 @@ func ConvertMCPServerToRemoteMCPServer(mcpServer *v1alpha1.MCPServer) (*v1alpha2
12681275
URL: fmt.Sprintf("http://%s.%s:%d/mcp", mcpServer.Name, mcpServer.Namespace, mcpServer.Spec.Deployment.Port),
12691276
Protocol: v1alpha2.RemoteMCPServerProtocolStreamableHttp,
12701277
},
1271-
}, nil
1278+
}
1279+
1280+
// Propagate the timeout from the MCPServer CRD to the generated
1281+
// RemoteMCPServer spec. Fall back to DefaultMCPServerTimeout for
1282+
// MCPServer objects created before the CRD default was introduced,
1283+
// so the ADK never uses its own 5s built-in which is too short for
1284+
// sidecar gateway cold starts.
1285+
if mcpServer.Spec.Timeout != nil {
1286+
remoteMCP.Spec.Timeout = mcpServer.Spec.Timeout
1287+
} else {
1288+
remoteMCP.Spec.Timeout = &metav1.Duration{Duration: DefaultMCPServerTimeout}
1289+
}
1290+
1291+
return remoteMCP, nil
12721292
}
12731293

12741294
func (a *adkApiTranslator) translateRemoteMCPServerTarget(ctx context.Context, agent *adk.AgentConfig, remoteMcpServer *v1alpha2.RemoteMCPServer, mcpServerTool *v1alpha2.McpServerTool, agentHeaders map[string]string, proxyURL string) error {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
operation: translateAgent
2+
targetObject: agent-with-proxy-mcpserver-timeout
3+
namespace: test
4+
proxyURL: http://proxy.kagent.svc.cluster.local:8080
5+
objects:
6+
- apiVersion: v1
7+
kind: Secret
8+
metadata:
9+
name: openai-secret
10+
namespace: test
11+
data:
12+
api-key: c2stdGVzdC1hcGkta2V5 # base64 encoded "sk-test-api-key"
13+
- apiVersion: kagent.dev/v1alpha2
14+
kind: ModelConfig
15+
metadata:
16+
name: default-model
17+
namespace: test
18+
spec:
19+
provider: OpenAI
20+
model: gpt-4o
21+
apiKeySecret: openai-secret
22+
apiKeySecretKey: api-key
23+
- apiVersion: kagent.dev/v1alpha1
24+
kind: MCPServer
25+
metadata:
26+
name: test-mcp-server
27+
namespace: test
28+
spec:
29+
deployment:
30+
port: 8084
31+
timeout: 60s
32+
- apiVersion: kagent.dev/v1alpha2
33+
kind: Agent
34+
metadata:
35+
name: agent-with-proxy-mcpserver-timeout
36+
namespace: test
37+
spec:
38+
type: Declarative
39+
declarative:
40+
description: An agent with proxy configuration and MCPServer with custom timeout
41+
systemMessage: You are an agent that uses proxies.
42+
modelConfig: default-model
43+
tools:
44+
- type: MCPServer
45+
mcpServer:
46+
name: test-mcp-server
47+
kind: MCPServer
48+
toolNames:
49+
- test-tool

go/internal/controller/translator/agent/testdata/outputs/agent_with_proxy_mcpserver.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"headers": {
2626
"x-kagent-host": "test-mcp-server.test"
2727
},
28+
"timeout": 30,
2829
"url": "http://proxy.kagent.svc.cluster.local:8080/mcp"
2930
},
3031
"tools": [
@@ -69,7 +70,7 @@
6970
},
7071
"stringData": {
7172
"agent-card.json": "{\"name\":\"agent_with_proxy_mcpserver\",\"description\":\"\",\"url\":\"http://agent-with-proxy-mcpserver.test:8080\",\"version\":\"\",\"capabilities\":{\"streaming\":true,\"pushNotifications\":false,\"stateTransitionHistory\":true},\"defaultInputModes\":[\"text\"],\"defaultOutputModes\":[\"text\"],\"skills\":[]}",
72-
"config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are an agent that uses proxies.\",\"http_tools\":[{\"params\":{\"url\":\"http://proxy.kagent.svc.cluster.local:8080/mcp\",\"headers\":{\"x-kagent-host\":\"test-mcp-server.test\"}},\"tools\":[\"test-tool\"]}],\"sse_tools\":null,\"remote_agents\":null,\"stream\":false}"
73+
"config.json": "{\"model\":{\"type\":\"openai\",\"model\":\"gpt-4o\",\"base_url\":\"\"},\"description\":\"\",\"instruction\":\"You are an agent that uses proxies.\",\"http_tools\":[{\"params\":{\"url\":\"http://proxy.kagent.svc.cluster.local:8080/mcp\",\"headers\":{\"x-kagent-host\":\"test-mcp-server.test\"},\"timeout\":30},\"tools\":[\"test-tool\"]}],\"sse_tools\":null,\"remote_agents\":null,\"stream\":false}"
7374
}
7475
},
7576
{
@@ -138,7 +139,7 @@
138139
"template": {
139140
"metadata": {
140141
"annotations": {
141-
"kagent.dev/config-hash": "4693175952507523169"
142+
"kagent.dev/config-hash": "14322610689340983156"
142143
},
143144
"labels": {
144145
"app": "kagent",

0 commit comments

Comments
 (0)