use crate::error::{McpError, McpResult};
use crate::protocol::{Resource, ResourceContent, ResourceReadResult, ResourcesListResult};
use serde_json::json;
use tracing::debug;
pub const INSTANCES_URI: &str = "aimdb://instances";
pub async fn list_resources() -> McpResult<ResourcesListResult> {
debug!("📋 Listing resources");
let mut resources = vec![Resource {
uri: INSTANCES_URI.to_string(),
name: "AimDB Instances".to_string(),
description: Some("List of all discovered AimDB instances with metadata".to_string()),
mime_type: Some("application/json".to_string()),
}];
match super::records::list_records_resources().await {
Ok(mut records_resources) => {
resources.append(&mut records_resources);
}
Err(e) => {
debug!("Could not list records resources: {}", e);
}
}
Ok(ResourcesListResult { resources })
}
pub async fn read_resource(uri: &str) -> McpResult<ResourceReadResult> {
debug!("📖 Reading resource: {}", uri);
match uri {
INSTANCES_URI => read_instances_resource().await,
_ if super::records::is_records_uri(uri) => {
if let Some(socket_path) = super::records::extract_socket_path(uri) {
super::records::read_records_resource(&socket_path).await
} else {
Err(McpError::InvalidParams(format!(
"Invalid records URI: {}",
uri
)))
}
}
_ => Err(McpError::InvalidParams(format!(
"Unknown resource URI: {}",
uri
))),
}
}
async fn read_instances_resource() -> McpResult<ResourceReadResult> {
debug!("🔍 Discovering instances for resource");
let instances = aimdb_client::discover_instances().await?;
debug!("✅ Found {} instance(s)", instances.len());
let instances_json: Vec<_> = instances
.into_iter()
.map(|info| {
json!({
"socket_path": info.socket_path.display().to_string(),
"server_version": info.server_version,
"protocol_version": info.protocol_version,
"permissions": info.permissions,
"writable_records": info.writable_records,
"max_subscriptions": info.max_subscriptions,
"authenticated": info.authenticated,
})
})
.collect();
let content_json = json!({
"instances": instances_json,
"count": instances_json.len(),
});
let content = ResourceContent {
uri: INSTANCES_URI.to_string(),
mime_type: Some("application/json".to_string()),
text: Some(serde_json::to_string_pretty(&content_json)?),
blob: None,
};
Ok(ResourceReadResult {
contents: vec![content],
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_list_resources() {
let result = list_resources().await.unwrap();
assert!(!result.resources.is_empty());
assert_eq!(result.resources[0].uri, INSTANCES_URI);
assert_eq!(result.resources[0].name, "AimDB Instances");
for resource in result.resources.iter().skip(1) {
assert!(resource.uri.starts_with("aimdb://"));
assert!(resource.uri.ends_with("/records"));
}
}
#[tokio::test]
async fn test_read_unknown_resource() {
let result = read_resource("aimdb://unknown").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_read_instances_resource() {
let result = read_resource(INSTANCES_URI).await;
match result {
Ok(read_result) => {
assert_eq!(read_result.contents.len(), 1);
assert_eq!(read_result.contents[0].uri, INSTANCES_URI);
assert!(read_result.contents[0].text.is_some());
}
Err(err) => {
assert!(err.message().contains("No running AimDB instances"));
}
}
}
}