sipeed/picoclaw · error

local command %q is a directory

Error message

local command %q is a directory

What it means

Thrown by validateLocalCommandPath in picoclaw's MCP helpers. When a configured local (stdio) MCP server command looks like a path (absolute, ./, ../, or containing a path separator), picoclaw expands ~ and stats it; this error means the path exists but is a directory. picoclaw needs an executable file to spawn as the server process, so it rejects a folder before any connection attempt. Bare command names like npx are never stat-ed and cannot produce this error.

Source

Thrown at cmd/picoclaw/internal/mcp/helpers.go:322

	}
	return path
}

func validateLocalCommandPath(command string) error {
	if !isLocalCommandPath(command) {
		return nil
	}

	path := expandHomePath(command)
	info, err := os.Stat(path)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return fmt.Errorf("local command %q does not exist", command)
		}
		return fmt.Errorf("failed to stat local command %q: %w", command, err)
	}
	if info.IsDir() {
		return fmt.Errorf("local command %q is a directory", command)
	}
	if runtime.GOOS != "windows" && info.Mode()&0o111 == 0 {
		return fmt.Errorf("local command %q is not executable", command)
	}
	return nil
}

func defaultServerProbe(
	ctx context.Context,
	name string,
	server config.MCPServerConfig,
	workspacePath string,
) (probeResult, error) {
	mgr := picomcp.NewManager()
	defer func() { _ = mgr.Close() }()

	server.Enabled = true
	mcpCfg := config.MCPConfig{

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Point command at the executable file itself (e.g. /usr/local/bin/weather-server, not /usr/local/bin) via picoclaw mcp remove + picoclaw mcp add or by editing the config
  2. Verify first: ls -ld <command> — a leading d in the mode column means it is a directory
  3. For package-run servers prefer wrapper commands such as npx -y <package> or uvx <package>, which need no path
  4. If the server is a script inside a folder, reference the script file plus its args, not the folder

Example fix

# before (config tools.mcp.servers.weather)
command: /home/dev/.local/share/mcp-servers

# after
command: /home/dev/.local/share/mcp-servers/weather-server
Defensive patterns

Strategy: validation

Validate before calling

func validateCommandPath(command string) error {
  if command == "" || !strings.ContainsRune(command, os.PathSeparator) {
    return nil // bare command names are not stat-ed by picoclaw either
  }
  path := command
  if strings.HasPrefix(path, "~") {
    home, _ := os.UserHomeDir()
    path = filepath.Join(home, strings.TrimPrefix(path, "~"))
  }
  info, err := os.Stat(path)
  if err != nil {
    return fmt.Errorf("command %s does not resolve: %w", command, err)
  }
  if info.IsDir() {
    return fmt.Errorf("command %s is a directory; point it at the executable file", command)
  }
  return nil
}

// call before saving the server config or running picoclaw mcp add

Type guard

func isCommandDirError(err error) bool {
  return err != nil && strings.Contains(err.Error(), "local command") && strings.Contains(err.Error(), "is a directory")
}

Try / catch

if err := runMCPAdd(cfg); err != nil {
  if isCommandDirError(err) {
    // prompt the user for the full executable path instead of the folder, then retry
  }
  return err
}

Prevention

When it happens

Trigger: An MCP server entry whose command is a directory, e.g. command: /usr/local/bin or command: ~/tools/mcp-server/; isLocalCommandPath returns true (path-like), os.Stat succeeds, and info.IsDir() is true.

Common situations: Pointing command at an install or bin directory (npm global bin dir, ~/.local/bin) instead of the binary inside it; pasting the folder path of a cloned/downloaded server; scaffolding a directory whose name collides with a relative command used from inside it.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/6bf8f181a54677a9. Report an issue: GitHub.