dapr/dapr · critical
failed to build transport for %q: %w
Error message
failed to build transport for %q: %w
What it means
Thrown by SessionHolder.connect when buildTransport cannot construct an MCP transport for the server. Looking at buildTransport (worker.go:277-314), the only failure path is the default branch: the MCPServer spec sets none of endpoint.streamableHTTP, endpoint.sse, or endpoint.stdio — the wrapped error reads 'no transport configured for MCPServer %q: set one of streamableHTTP, sse, or stdio'. It fires at eager connect, i.e. registration time.
Source
Thrown at pkg/runtime/wfengine/inprocess/mcp/v1/session.go:154
defer h.mu.Unlock()
if s := h.session.Load(); s != nil {
(*s).Close()
h.session.Store(nil)
}
}
// connect builds an HTTP client, transport, and MCP session.
// The caller's context controls the connection deadline.
// The lifecycleCtx is passed separately for background work (token refresh) that must outlive the connect call.
func (h *SessionHolder) connect(ctx context.Context) (*mcp.ClientSession, error) {
httpClient, err := mcpauth.BuildHTTPClient(ctx, h.lifecycleCtx, h.server, h.store, h.sec)
if err != nil {
return nil, fmt.Errorf("failed to build HTTP client for %q: %w", h.server.Name, err)
}
transport, err := buildTransport(h.server, httpClient)
if err != nil {
return nil, fmt.Errorf("failed to build transport for %q: %w", h.server.Name, err)
}
workerLog.Debugf("connecting to MCP server %q", h.server.Name)
c := mcp.NewClient(&mcp.Implementation{Name: mcpClientName, Version: mcpClientVersion}, &mcp.ClientOptions{
KeepAlive: keepAliveInterval,
})
session, err := c.Connect(ctx, transport, nil)
if err != nil {
return nil, fmt.Errorf("failed to connect to MCP server %q: %w", h.server.Name, err)
}
return session, nil
}
// isConnectionClosed returns true if the error wraps mcp.ErrConnectionClosed.
func isConnectionClosed(err error) bool {
return errors.Is(err, mcp.ErrConnectionClosed)
}
View on GitHub (pinned to 74ad417027)
Solutions
- Read the wrapped message — it tells you to set one of streamableHTTP, sse, or stdio
- Inspect the applied object (kubectl get mcpserver <name> -o yaml) to see what endpoint actually persisted after defaults/pruning
- Fix the manifest so exactly one endpoint block carries the URL/command; re-apply
- Verify you are on a dapr version whose MCPServer CRD matches the field names you used
- After fixing, the operator's hot-reload re-runs registration — confirm the holder connects
Example fix
# before: no endpoint transport block
apiVersion: dapr.io/v1alpha1
kind: MCPServer
metadata: { name: fs }
spec:
endpoint: {}
# after: streamableHTTP transport set
spec:
endpoint:
streamableHTTP:
url: http://filesystem-mcp:8080/mcp Defensive patterns
Strategy: validation
Validate before calling
// Reject manifests without exactly one transport before registration
func validateTransport(srv *mcpserverapi.MCPServer) error {
n := 0
for _, set := range []bool{
srv.Spec.Endpoint.StreamableHTTP != nil,
srv.Spec.Endpoint.SSE != nil,
srv.Spec.Endpoint.Stdio != nil,
} { if set { n++ } }
if n != 1 {
return fmt.Errorf("MCPServer %q must set exactly one of streamableHTTP, sse, stdio (got %d)", srv.Name, n)
}
return nil
} Type guard
func hasTransport(srv *mcpserverapi.MCPServer) bool {
return srv.Spec.Endpoint.StreamableHTTP != nil ||
srv.Spec.Endpoint.SSE != nil ||
srv.Spec.Endpoint.Stdio != nil
} Try / catch
transport, err := buildTransport(server, httpClient)
if err != nil {
return nil, fmt.Errorf("failed to build transport for %q: %w", server.Name, err)
// permanent config error: do not retry; fix the manifest
} Prevention
- Add admission/CI validation that every MCPServer manifest sets exactly one endpoint block
- Diff the persisted object (kubectl get -o yaml) after apply to catch pruned/typo'd fields
- Pin manifests to the CRD version of the deployed dapr release
When it happens
Trigger: Applying an MCPServer manifest whose spec.endpoint omits all three transport blocks (or contains only unrelated fields), then Dapr attempts to create the session holder and register workflows.
Common situations: Manifest authored with a typo'd or misplaced endpoint field (e.g. url at the wrong level so the block is empty after unmarshal); CRD schema drift between dapr versions renaming endpoint fields; manifests generated by tooling that drops empty blocks; kubectl apply pruning fields unknown to the stored CRD version.
Related errors
- auth.oauth2 is configured but no secret store is available
- auth.oauth2.secretKeyRef is required for OAuth2 client crede
- MCPServer %q failed validation: %w
- MCPServer %q failed security validation: %w
- incorrect type %s
AI-assisted analysis of dapr/dapr@74ad417027 (2026-08-16).
Data as JSON: /api/errors/7cf2d053d8733d7a.
Report an issue: GitHub.