use anyhow::{Context, Result};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
#[derive(Debug, Deserialize)]
struct ClaudeConfig {
#[serde(default)]
projects: HashMap<String, ProjectConfig>,
}
#[derive(Debug, Deserialize)]
struct ProjectConfig {
#[serde(default, rename = "mcpServers")]
mcp_servers: HashMap<String, McpServer>,
}
#[derive(Debug, Deserialize)]
struct McpServer {
#[serde(default)]
env: HashMap<String, String>,
}
pub fn get_github_token() -> Result<String> {
let config_path = crate::paths::claude_config_path();
let content = fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read {}", config_path.display()))?;
let config: ClaudeConfig = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse {}", config_path.display()))?;
for project_config in config.projects.values() {
if let Some(github_server) = project_config.mcp_servers.get("github")
&& let Some(token) = github_server.env.get("GITHUB_PERSONAL_ACCESS_TOKEN")
&& !token.is_empty()
{
return Ok(token.clone());
}
}
anyhow::bail!(
"GitHub token not found in {}\n\
Expected path: projects.<project>.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN",
config_path.display()
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_path() {
let path = crate::paths::claude_config_path();
assert!(path.ends_with(".claude.json"));
}
#[test]
#[ignore]
fn test_get_token_from_config() {
let token = get_github_token().expect("Should find token");
assert!(!token.is_empty());
assert!(
token.starts_with("ghp_")
|| token.starts_with("gho_")
|| token.starts_with("github_pat_")
);
}
}