charmbracelet/crush · error

mcp '%s' not available

Error message

mcp '%s' not available

What it means

getOrRenewClient looks up the named MCP session in the sessions registry; if absent it reports the server as unavailable. This check is deliberately done after acquiring the lock because a concurrent renewal transiently removes/errored sessions, and callers arriving mid-renewal must not see a spurious 'not available'.

Source

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

	// Serialize renewals per server. Two concurrent tool calls can both
	// observe a dead session and race to rebuild it: one may close the
	// session the other just registered, or overwrite and leak a live
	// replacement. Under this lock only the first arrival rebuilds; later
	// arrivals re-check and reuse the healthy result.
	mu := renewLock(name)
	mu.Lock()
	defer mu.Unlock()

	// Under the lock the map is stable: any in-flight renewal has finished and
	// either re-registered its session or failed and left none. A renewal
	// removes the session transiently (StateError takes it before rebuilding),
	// so this check must happen here rather than before the lock — otherwise a
	// caller arriving mid-renewal sees no session and wrongly reports the
	// server unavailable.
	sess, ok := sessions.Get(name)
	if !ok {
		return nil, fmt.Errorf("mcp '%s' not available", name)
	}

	// A concurrent goroutine may have already renewed the session while we
	// waited for the lock. Reuse it if it is now healthy.
	pingErr := pingSession(ctx, sess, timeout)
	if pingErr == nil {
		return sess, nil
	}

	state, _ := states.Get(name)
	// StateError closes the dead session and clears its tools, prompts, and
	// resources from the registry.
	updateState(name, StateError, maybeTimeoutErr(pingErr, timeout), nil, state.Counts)

	// Capture the generation so a reconcile teardown that lands mid-renewal
	// invalidates this rebuild instead of letting it clobber the newer one.
	gen := currentGen(name)
	newSess, err := newSession(ctx, cfg, name, m, cfg.Resolver(), channelEnabled(cfg.Overrides().EnabledChannels, name))

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Verify the MCP server name matches an entry in your config (mcp builtin / crush.json)
  2. Check startup logs for that server's init failure and fix the underlying cause
  3. Retry after the renewal completes if the call raced a reconnection
  4. List configured MCP servers (crush models / config output) to confirm the name exists

Example fix

// before: calling with a wrong name
client.RunTool(ctx, "github-mcp", "create_issue", args)

// after: name matches an mcp entry in config
client.RunTool(ctx, "github", "create_issue", args)
Defensive patterns

Strategy: validation

Validate before calling

func clientReady(sessions *SessionRegistry, name string) error {
    if _, ok := sessions.Get(name); !ok {
        return fmt.Errorf("mcp '%s' is not configured or failed to start; check your mcp config", name)
    }
    return nil
}
// call before RunTool/ListResources/etc.

Try / catch

if err := client.RunTool(ctx, name, tool, args); err != nil && strings.Contains(err.Error(), "not available") {
    // re-check config / wait for init, then retry once
}

Prevention

When it happens

Trigger: Calling RunTool, ListResources, ReadResource, or GetPromptMessages for an MCP server name that was never configured, failed init (StateError and removed), or is being rebuilt concurrently and the session is missing.

Common situations: Typo in the MCP server name in config or tool invocation; the server failed at startup (see init errors); agent tool called before the client finished initializing; server was removed from config while a session was in flight.

Related errors


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