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

  1. Run `/mcp import` and copy the exact approve or decline command it prints.
  2. Paste the whole token as a single shell argument (quote it if it contains special characters).
  3. 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

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


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)