use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Harness {
ClaudeCode,
Codex,
Cursor,
Cline,
Continue,
Aider,
Goose,
ClaudeDesktop,
Generic(String),
}
impl Harness {
#[must_use]
pub fn detect(client_name: &str) -> Self {
let normalised = client_name
.chars()
.filter(|c| !matches!(c, '-' | '_' | ' ' | '.'))
.flat_map(char::to_lowercase)
.collect::<String>();
if normalised.contains("claudecode") {
Self::ClaudeCode
} else if normalised.contains("claudedesktop") {
Self::ClaudeDesktop
} else if normalised.contains("codex") {
Self::Codex
} else if normalised.contains("cursor") {
Self::Cursor
} else if normalised.contains("cline") {
Self::Cline
} else if normalised.contains("continue") {
Self::Continue
} else if normalised.contains("aider") {
Self::Aider
} else if normalised.contains("goose") {
Self::Goose
} else {
Self::Generic(client_name.to_string())
}
}
#[must_use]
pub fn supports_deferred_registration(&self) -> bool {
match self {
Self::ClaudeCode => true,
Self::Codex
| Self::Cursor
| Self::Cline
| Self::Continue
| Self::Aider
| Self::Goose
| Self::ClaudeDesktop
| Self::Generic(_) => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_claude_code_canonical_kebab() {
assert_eq!(Harness::detect("claude-code"), Harness::ClaudeCode);
}
#[test]
fn detect_claude_code_title_case_with_space() {
assert_eq!(Harness::detect("Claude Code"), Harness::ClaudeCode);
}
#[test]
fn detect_claude_code_snake_case() {
assert_eq!(Harness::detect("claude_code"), Harness::ClaudeCode);
}
#[test]
fn detect_claude_code_screaming_with_dots() {
assert_eq!(Harness::detect("CLAUDE.CODE"), Harness::ClaudeCode);
}
#[test]
fn detect_claude_code_versioned_suffix() {
assert_eq!(Harness::detect("claude-code-cli"), Harness::ClaudeCode);
assert_eq!(Harness::detect("claude-code/1.2.3"), Harness::ClaudeCode);
}
#[test]
fn detect_codex_variants() {
assert_eq!(Harness::detect("codex"), Harness::Codex);
assert_eq!(Harness::detect("Codex"), Harness::Codex);
assert_eq!(Harness::detect("codex-cli"), Harness::Codex);
assert_eq!(Harness::detect("openai-codex"), Harness::Codex);
}
#[test]
fn detect_cursor_variants() {
assert_eq!(Harness::detect("cursor"), Harness::Cursor);
assert_eq!(Harness::detect("Cursor"), Harness::Cursor);
assert_eq!(Harness::detect("cursor-mcp"), Harness::Cursor);
}
#[test]
fn detect_cline_variants() {
assert_eq!(Harness::detect("cline"), Harness::Cline);
assert_eq!(Harness::detect("Cline"), Harness::Cline);
assert_eq!(Harness::detect("vscode-cline"), Harness::Cline);
}
#[test]
fn detect_continue_variants() {
assert_eq!(Harness::detect("continue"), Harness::Continue);
assert_eq!(Harness::detect("Continue"), Harness::Continue);
assert_eq!(Harness::detect("continue.dev"), Harness::Continue);
}
#[test]
fn detect_aider_variants() {
assert_eq!(Harness::detect("aider"), Harness::Aider);
assert_eq!(Harness::detect("Aider"), Harness::Aider);
assert_eq!(Harness::detect("aider-cli"), Harness::Aider);
}
#[test]
fn detect_goose_variants() {
assert_eq!(Harness::detect("goose"), Harness::Goose);
assert_eq!(Harness::detect("Goose"), Harness::Goose);
assert_eq!(Harness::detect("block-goose"), Harness::Goose);
}
#[test]
fn detect_claude_desktop_variants() {
assert_eq!(Harness::detect("claude-desktop"), Harness::ClaudeDesktop);
assert_eq!(Harness::detect("Claude Desktop"), Harness::ClaudeDesktop);
assert_eq!(Harness::detect("ClaudeDesktop"), Harness::ClaudeDesktop);
}
#[test]
fn detect_unknown_preserves_original_name() {
let raw = "MyCustomMcpClient/0.1";
let h = Harness::detect(raw);
match h {
Harness::Generic(s) => assert_eq!(s, raw),
other => panic!("expected Generic; got {other:?}"),
}
}
#[test]
fn detect_empty_name_is_generic() {
assert_eq!(Harness::detect(""), Harness::Generic(String::new()));
}
#[test]
fn deferred_registration_only_claude_code_today() {
assert!(Harness::ClaudeCode.supports_deferred_registration());
assert!(!Harness::Codex.supports_deferred_registration());
assert!(!Harness::Cursor.supports_deferred_registration());
assert!(!Harness::Cline.supports_deferred_registration());
assert!(!Harness::Continue.supports_deferred_registration());
assert!(!Harness::Aider.supports_deferred_registration());
assert!(!Harness::Goose.supports_deferred_registration());
assert!(!Harness::ClaudeDesktop.supports_deferred_registration());
}
#[test]
fn deferred_registration_unknown_defaults_false() {
assert!(
!Harness::Generic("some-random-mcp-client".to_string())
.supports_deferred_registration()
);
assert!(!Harness::Generic(String::new()).supports_deferred_registration());
}
#[test]
fn serde_round_trips_named_variants() {
let h = Harness::ClaudeCode;
let s = serde_json::to_string(&h).expect("serialize");
assert_eq!(s, "\"claude_code\"");
let back: Harness = serde_json::from_str(&s).expect("deserialize");
assert_eq!(back, Harness::ClaudeCode);
}
#[test]
fn serde_round_trips_generic_variant() {
let h = Harness::Generic("foo".to_string());
let s = serde_json::to_string(&h).expect("serialize");
let back: Harness = serde_json::from_str(&s).expect("deserialize");
assert_eq!(back, h);
}
}