use crate::mcp::error::{McpError, McpResult};
use crate::tools::spec::{generate_tool_index, get_tool_spec};
use serde::Serialize;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct McpResourceDefinition {
pub uri: String,
pub name: String,
pub description: String,
pub mime_type: String,
}
pub fn list_resources() -> Vec<McpResourceDefinition> {
vec![
McpResourceDefinition {
uri: "jarvy://tools/index".to_string(),
name: "Tool Index".to_string(),
description: "Complete index of all supported tools with metadata".to_string(),
mime_type: "application/json".to_string(),
},
McpResourceDefinition {
uri: "jarvy://platform/info".to_string(),
name: "Platform Info".to_string(),
description: "Current platform, OS version, and available package managers".to_string(),
mime_type: "application/json".to_string(),
},
McpResourceDefinition {
uri: "jarvy://config".to_string(),
name: "Project Config".to_string(),
description: "Parsed jarvy.toml configuration for the current project".to_string(),
mime_type: "application/json".to_string(),
},
McpResourceDefinition {
uri: "jarvy://doctor".to_string(),
name: "Environment Health".to_string(),
description: "Doctor diagnostics for configured tools (installed, versions, issues)"
.to_string(),
mime_type: "application/json".to_string(),
},
McpResourceDefinition {
uri: "jarvy://schema".to_string(),
name: "Config Schema".to_string(),
description: "JSON Schema for jarvy.toml (for editor autocomplete and validation)"
.to_string(),
mime_type: "application/schema+json".to_string(),
},
]
}
pub fn read_resource(uri: &str) -> McpResult<String> {
match uri {
"jarvy://tools/index" => read_tools_index(),
"jarvy://platform/info" => read_platform_info(),
"jarvy://config" => read_project_config(),
"jarvy://doctor" => read_doctor_results(),
"jarvy://schema" => read_config_schema(),
_ if uri.starts_with("jarvy://tools/") => {
let tool_name = uri.strip_prefix("jarvy://tools/").unwrap();
read_tool_details(tool_name)
}
_ => Err(McpError::invalid_params(format!(
"Unknown resource URI: {}",
uri
))),
}
}
fn read_tools_index() -> McpResult<String> {
let index = generate_tool_index();
serde_json::to_string_pretty(&index).map_err(|e| McpError::internal_error(e.to_string()))
}
fn read_platform_info() -> McpResult<String> {
let info = PlatformInfo::detect();
serde_json::to_string_pretty(&info).map_err(|e| McpError::internal_error(e.to_string()))
}
fn read_tool_details(tool_name: &str) -> McpResult<String> {
let spec = get_tool_spec(tool_name).ok_or_else(|| McpError::unknown_tool(tool_name))?;
let index = generate_tool_index();
let tool_entry = index.tools.iter().find(|t| t.name == tool_name);
let details = serde_json::json!({
"name": tool_name,
"command": spec.command,
"platforms": tool_entry.map(|t| serde_json::json!({
"macos": t.macos,
"linux": t.linux,
"windows": t.windows
})),
"custom_install": tool_entry.map(|t| t.custom_install.has_custom_installer).unwrap_or(false),
});
serde_json::to_string_pretty(&details).map_err(|e| McpError::internal_error(e.to_string()))
}
#[derive(Debug, Serialize)]
struct PlatformInfo {
os: String,
os_version: Option<String>,
arch: String,
package_managers: Vec<PackageManagerInfo>,
}
#[derive(Debug, Serialize)]
struct PackageManagerInfo {
name: String,
installed: bool,
version: Option<String>,
}
impl PlatformInfo {
fn detect() -> Self {
let os = detect_os();
let arch = detect_arch();
let os_version = detect_os_version();
let package_managers = detect_package_managers(&os);
Self {
os,
os_version,
arch,
package_managers,
}
}
}
fn detect_os() -> String {
#[cfg(target_os = "macos")]
return "macos".to_string();
#[cfg(target_os = "linux")]
return "linux".to_string();
#[cfg(target_os = "windows")]
return "windows".to_string();
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
return std::env::consts::OS.to_string();
}
fn detect_arch() -> String {
std::env::consts::ARCH.to_string()
}
fn detect_os_version() -> Option<String> {
#[cfg(target_os = "macos")]
{
std::process::Command::new("sw_vers")
.arg("-productVersion")
.output()
.ok()
.and_then(|o| {
if o.status.success() {
Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
} else {
None
}
})
}
#[cfg(target_os = "linux")]
{
std::fs::read_to_string("/etc/os-release")
.ok()
.and_then(|content| {
for line in content.lines() {
if line.starts_with("VERSION_ID=") {
return Some(
line.trim_start_matches("VERSION_ID=")
.trim_matches('"')
.to_string(),
);
}
}
None
})
}
#[cfg(target_os = "windows")]
{
std::process::Command::new("cmd")
.args(["/C", "ver"])
.output()
.ok()
.and_then(|o| {
if o.status.success() {
Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
} else {
None
}
})
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
None
}
fn detect_package_managers(os: &str) -> Vec<PackageManagerInfo> {
let mut managers = Vec::new();
match os {
"macos" => {
managers.push(check_package_manager("brew", &["--version"]));
managers.push(check_package_manager("port", &["version"]));
}
"linux" => {
managers.push(check_package_manager("apt", &["--version"]));
managers.push(check_package_manager("dnf", &["--version"]));
managers.push(check_package_manager("yum", &["--version"]));
managers.push(check_package_manager("pacman", &["--version"]));
managers.push(check_package_manager("apk", &["--version"]));
managers.push(check_package_manager("zypper", &["--version"]));
}
"windows" => {
managers.push(check_package_manager("winget", &["--version"]));
managers.push(check_package_manager("choco", &["--version"]));
managers.push(check_package_manager("scoop", &["--version"]));
}
_ => {}
}
managers.push(check_package_manager("npm", &["--version"]));
managers.push(check_package_manager("pip", &["--version"]));
managers.push(check_package_manager("cargo", &["--version"]));
managers.into_iter().filter(|m| m.installed).collect()
}
fn check_package_manager(name: &str, version_args: &[&str]) -> PackageManagerInfo {
let output = std::process::Command::new(name).args(version_args).output();
match output {
Ok(o) if o.status.success() => {
let version_output = String::from_utf8_lossy(&o.stdout);
let version = extract_version_number(&version_output);
PackageManagerInfo {
name: name.to_string(),
installed: true,
version,
}
}
_ => PackageManagerInfo {
name: name.to_string(),
installed: false,
version: None,
},
}
}
fn extract_version_number(output: &str) -> Option<String> {
for word in output.split_whitespace() {
let word = word.trim_start_matches('v').trim_end_matches(',');
if word.chars().next().is_some_and(|c| c.is_ascii_digit()) && word.contains('.') {
return Some(word.to_string());
}
}
output.lines().next().map(|s| s.trim().to_string())
}
fn read_project_config() -> McpResult<String> {
let config_path = "./jarvy.toml";
let content = std::fs::read_to_string(config_path)
.map_err(|e| McpError::internal_error(format!("Cannot read {}: {}", config_path, e)))?;
let parsed: toml::Value = toml::from_str(&content)
.map_err(|e| McpError::internal_error(format!("Invalid TOML: {}", e)))?;
serde_json::to_string_pretty(&parsed).map_err(|e| McpError::internal_error(e.to_string()))
}
fn read_doctor_results() -> McpResult<String> {
let result = crate::commands::doctor::run_doctor(None, None);
serde_json::to_string_pretty(&result).map_err(|e| McpError::internal_error(e.to_string()))
}
fn read_config_schema() -> McpResult<String> {
let schema_output = crate::commands::schema::generate_schema();
serde_json::to_string_pretty(&schema_output.schema)
.map_err(|e| McpError::internal_error(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_list_resources() {
let resources = list_resources();
assert!(!resources.is_empty());
assert!(resources.iter().any(|r| r.uri == "jarvy://tools/index"));
assert!(resources.iter().any(|r| r.uri == "jarvy://platform/info"));
}
#[test]
fn test_read_tools_index() {
crate::tools::register_all();
let result = read_resource("jarvy://tools/index");
assert!(result.is_ok());
let json = result.unwrap();
assert!(json.contains("tools"));
}
#[test]
fn test_read_platform_info() {
let result = read_resource("jarvy://platform/info");
assert!(result.is_ok());
let json = result.unwrap();
assert!(json.contains("os"));
assert!(json.contains("arch"));
}
#[test]
fn test_read_unknown_resource() {
let result = read_resource("jarvy://unknown/resource");
assert!(result.is_err());
}
#[test]
fn test_detect_os() {
let os = detect_os();
#[cfg(target_os = "macos")]
assert_eq!(os, "macos");
#[cfg(target_os = "linux")]
assert_eq!(os, "linux");
#[cfg(target_os = "windows")]
assert_eq!(os, "windows");
}
}