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

  1. Read the wrapped message — it tells you to set one of streamableHTTP, sse, or stdio
  2. Inspect the applied object (kubectl get mcpserver <name> -o yaml) to see what endpoint actually persisted after defaults/pruning
  3. Fix the manifest so exactly one endpoint block carries the URL/command; re-apply
  4. Verify you are on a dapr version whose MCPServer CRD matches the field names you used
  5. 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

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


AI-assisted analysis of dapr/dapr@74ad417027 (2026-08-16). Data as JSON: /api/errors/7cf2d053d8733d7a. Report an issue: GitHub.