Hmbown/CodeWhale · error
MCP config path must be a regular file
Error message
MCP config path must be a regular file: {} What it means
The MCP config loader validates that the given path is a plain regular file; symlinks and non-file entries (directories, sockets, fifos) are rejected. This prevents loading config through indirection that could bypass security or integrity expectations.
Solutions
- Replace the symlink with a real file (copy the target contents to the path)
- Point the MCP config path at a regular file instead of a directory or special file
- If a symlink is unavoidable, hard-link or copy the file rather than symlinking
Example fix
// before ln -s ~/dotfiles/mcp.json ~/.codewhale/mcp.json // after cp ~/dotfiles/mcp.json ~/.codewhale/mcp.json
Defensive patterns
Strategy: validation
Validate before calling
let md = std::fs::symlink_metadata(path)?;
if md.file_type().is_symlink() || !md.is_file() {
return Err(format!("{} must be a regular file", path.display()));
} Type guard
fn is_regular_file(path: &Path) -> bool {
std::fs::symlink_metadata(path)
.map(|md| md.file_type().is_file()).unwrap_or(false)
} Try / catch
match load_mcp_config(path) {
Err(e) if e.to_string().contains("must be a regular file") => {
eprintln!("resolve the symlink or point at a real file: {path:?}");
}
other => other?,
} Prevention
- Don't symlink config files managed by dotfile tools; copy them instead
- Use symlink_metadata (not metadata) in pre-checks so symlinks are detected
- Validate the path is a file before handing it to the config loader
When it happens
Trigger: Passing a path to the MCP config loader where metadata.file_type().is_symlink() is true or !is_file() — e.g. a symlinked ~/.codewhale/mcp.json, or pointing at a directory.
Common situations: Dotfile managers (stow, chezmoi, GNU stow) that symlink config files into the repo; accidentally passing a directory path; mounting the config as a socket/fifo.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Failed to read MCP config
- Failed to write MCP config
- project-scope config must not be a symlink
- reviewed plugin cwd is a reparse point or non-directory
- built-in plugin path may not be a symbolic link or reparse…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/4a56b3369da46fd5.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/mcp.rs:5347
)
})
}
/// Maximum bytes read from an MCP config file. Configs are kilobytes.
const MAX_MCP_CONFIG_BYTES: u64 = 1024 * 1024;
fn read_mcp_config_file(path: &Path) -> Result<Option<String>> {
let metadata = match fs::symlink_metadata(path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(err) => {
return Err(err)
.with_context(|| format!("Failed to inspect MCP config {}", path.display()));
}
};
let file_type = metadata.file_type();
if file_type.is_symlink() || !file_type.is_file() {
anyhow::bail!("MCP config path must be a regular file: {}", path.display());
}
let file = open_mcp_config_file(path)
.with_context(|| format!("Failed to read MCP config {}", path.display()))?;
let mut contents = String::new();
file.take(MAX_MCP_CONFIG_BYTES + 1)
.read_to_string(&mut contents)
.with_context(|| format!("Failed to read MCP config {}", path.display()))?;
if contents.len() as u64 > MAX_MCP_CONFIG_BYTES {
anyhow::bail!("MCP config {} exceeds the 1 MiB limit", path.display());
}
Ok(Some(contents))
}
#[cfg(unix)]
fn open_mcp_config_file(path: &Path) -> std::io::Result<fs::File> {
use std::os::unix::fs::OpenOptionsExt;
View on GitHub (pinned to 73e0f67d83)