charmbracelet/crush · error

mcp sse config requires a non-empty 'url' field

Error message

mcp sse config requires a non-empty 'url' field

What it means

During MCP client transport construction for an SSE server, the resolved URL is checked after shell-variable resolution. If the URL is empty or only whitespace, startup aborts because an SSE client transport cannot dial a blank endpoint. This catches configs where the `url` key is missing, empty, or resolves (via $VAR/$(cmd)) to nothing.

Source

Thrown at internal/agent/tools/mcp/init.go:1137

		if err != nil {
			return nil, nil, err
		}
		client := &http.Client{
			Transport: &headerRoundTripper{
				headers: headers,
			},
		}
		return &mcp.StreamableClientTransport{
			Endpoint:   url,
			HTTPClient: client,
		}, nil, nil
	case config.MCPSSE:
		url, err := m.ResolvedURL(resolver)
		if err != nil {
			return nil, nil, err
		}
		if strings.TrimSpace(url) == "" {
			return nil, nil, fmt.Errorf("mcp sse config requires a non-empty 'url' field")
		}
		headers, err := m.ResolvedHeaders(resolver)
		if err != nil {
			return nil, nil, err
		}

		var transport http.RoundTripper = &headerRoundTripper{headers: headers}
		var oauthHandler *mcpoauth.Handler

		// SSE transports don't support the SDK's OAuthHandler natively,
		// so we wrap the HTTP transport with our own round-tripper that
		// injects bearer tokens and handles 401-triggered authorization.
		// Based on Bruno Krugel's oauthRoundTripper from PR #3396.
		if m.OAuth {
			tokenSaver := func(tok *oauth.Token) {
				if err := cfg.SetConfigField(config.ScopeGlobal, fmt.Sprintf("mcp.%s.oauth_token", name), tok); err != nil {
					slog.Warn("Failed to persist MCP OAuth token", "name", name, "error", err)
				} else {

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Set a non-empty `url` field for the sse MCP server in crushrc/crush.json, e.g. url = "https://example.com/sse"
  2. If url uses $VAR or $(cmd), verify the variable is exported and non-empty in the environment Crush launches from
  3. Run `crush` with the same shell profile that defines the variable, or inline the literal URL
  4. Use `echo $MCP_URL` in the same shell to confirm it expands to a value before starting Crush

Example fix

// before (crushrc)
mcp context7 {
  type sse
  url ""
}
// after
mcp context7 {
  type sse
  url "https://mcp.context7.com/sse"
}
Defensive patterns

Strategy: validation

Validate before calling

// before starting crush, resolve like the library does
url := os.Getenv("MCP_URL")
if strings.TrimSpace(url) == "" {
    return fmt.Errorf("mcp sse url must be set: MCP_URL=%q", url)
}

Try / catch

// Go: wrap startup so a failing MCP does not kill the app
if _, _, err := mcp.CreateTransport(ctx, cfg, name); err != nil {
    slog.Warn("MCP transport unavailable", "name", name, "error", err)
}

Prevention

When it happens

Trigger: Defining an MCP server with type = "sse" whose `url` field is absent, an empty string, or a shell expression like "${MCP_URL}" that resolves to an empty value; `m.ResolvedURL` succeeds but TrimSpace(url) == "" at transport creation.

Common situations: Environment variable not exported in the shell Crush runs from; typo in the variable name inside the crushrc url field; user forgot to add the url key when converting an old HTTP proxy config to sse type.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/53d0acf282d1a225. Report an issue: GitHub.