Hmbown/CodeWhale · error · anyhow
Approval needs a reviewed token, not a name. Run /mcp…
Error message
Approval needs a reviewed token, not a name. Run /mcp import and copy its approve or decline command
What it means
`parse_review_token` validates a strict token format: id, revision, a 64-hex-char hash, and `mcp-v1-` prefixed hash segment. If the user passed a plain source name or a malformed string instead of a token copied from `/mcp import` output, this error fires before any decision is made.
Solutions
- Run `/mcp import` and copy the exact approve or decline command it prints.
- Paste the whole token as a single shell argument (quote it if it contains special characters).
- Verify the token has the expected segments: id, revision, and 64-hex hashes including the `mcp-v1-` prefix.
Example fix
// before codewhale mcp import apply my-source-server // after codewhale mcp import apply "a1b2... 3 rev mcp-v1-64hexhash" # token copied from /mcp import
Defensive patterns
Strategy: validation
Validate before calling
fn looks_like_review_token(s: &str) -> bool {
let parts: Vec<&str> = s.split_whitespace().collect();
parts.len() == 4
&& parts[2].len() == 64 && parts[2].bytes().all(|b| b.is_ascii_hexdigit())
&& (parts[3] == "mcp-v1-absent"
|| parts[3].starts_with("mcp-v1-"))
}
if !looks_like_review_token(arg) { eprintln!("pass the token copied from /mcp import"); } Type guard
fn is_review_token(s: &str) -> bool {
let p: Vec<&str> = s.split_whitespace().collect();
p.len() == 4
&& p[2].len() == 64 && p[2].bytes().all(|b| b.is_ascii_hexdigit())
&& (p[3] == "mcp-v1-absent"
|| p[3].strip_prefix("mcp-v1-").is_some_and(|h| h.len() == 64 && h.bytes().all(|b| b.is_ascii_hexdigit())))
} Try / catch
match mcp_import_apply(raw) {
Err(e) if e.to_string().contains("needs a reviewed token") => {
// show /mcp import output and let the user copy the exact command
}
other => other?,
} Prevention
- Always copy the approve/decline command verbatim from /mcp import output
- Quote tokens in shell invocations
- Do not retype long tokens from memory
When it happens
Trigger: Running `mcp import apply <name>` with a source name instead of the approve/decline token; copy-pasting only part of the token; quoting/shell-mangling that splits the token.
Common situations: Typing the command by hand from memory instead of copying it; older docs/examples showing name-based apply; shell history truncating a long token.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- A positive pull request number is required
- API key contains invalid control characters
- API key id must be lowercase hex characters — the part…
- API key input is unexpectedly large
- API key must be - UTF-8 bytes
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/d193e2da698172a9.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/mcp/external_import.rs:729
Ok(ImportReceipt { name: candidate.name, decision, imported: decision == ImportDecision::Approve,
enabled: false, revision, consent_recorded,
warning: (!consent_recorded).then(|| "The decision could not be added to import history; the configuration receipt above is authoritative".into()),
})
}
pub fn parse_review_token(token: &str) -> anyhow::Result<(&str, &str, &str)> {
let parts: Vec<_> = token.split(':').collect();
anyhow::ensure!(
parts.len() == 4
&& parts[0] == "mcp-import-v1"
&& parts[1..3]
.iter()
.all(|value| value.len() == 64 && value.bytes().all(|b| b.is_ascii_hexdigit()))
&& (parts[3] == "mcp-v1-absent"
|| parts[3].strip_prefix("mcp-v1-").is_some_and(
|hash| hash.len() == 64 && hash.bytes().all(|b| b.is_ascii_hexdigit())
)),
"Approval needs a reviewed token, not a name. Run /mcp import and copy its approve or decline command"
);
Ok((parts[1], parts[2], parts[3]))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn write_claude_json(dir: &Path, body: &str) -> PathBuf {
let path = dir.join(".claude.json");
fs::write(&path, body).unwrap();
path
}
fn with_import_context(test: impl FnOnce(&ImportContext<'_>)) {
let _env = crate::test_support::lock_test_env();
let root = tempdir().unwrap();View on GitHub (pinned to 73e0f67d83)