Files
crank/apps/mcp-server/src/catalog.rs
T
2026-05-04 05:22:27 +00:00

207 lines
5.7 KiB
Rust

use std::{
collections::HashMap,
sync::Arc,
time::{Duration, Instant},
};
use crank_core::{CacheScope, CoordinationStateStore, CoordinationStateValue};
use crank_registry::{PostgresRegistry, PublishedAgentTool, RegistryError};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use tracing::info;
#[derive(Clone)]
pub struct PublishedToolCatalog {
registry: PostgresRegistry,
refresh_interval: Duration,
coordination_store: Arc<dyn CoordinationStateStore>,
cached: Arc<RwLock<HashMap<CatalogKey, CachedCatalog>>>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct CatalogKey {
workspace_slug: String,
agent_slug: String,
}
#[derive(Default)]
struct CachedCatalog {
loaded_at: Option<Instant>,
tools: Vec<PublishedAgentTool>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct CatalogSnapshot {
tools: Vec<PublishedAgentTool>,
}
impl PublishedToolCatalog {
pub fn new(
registry: PostgresRegistry,
refresh_interval: Duration,
coordination_store: Arc<dyn CoordinationStateStore>,
) -> Self {
Self {
registry,
refresh_interval,
coordination_store,
cached: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn list_tools(
&self,
workspace_slug: &str,
agent_slug: &str,
) -> Result<Vec<PublishedAgentTool>, RegistryError> {
self.refresh_if_stale(workspace_slug, agent_slug).await?;
let guard = self.cached.read().await;
Ok(guard
.get(&CatalogKey::new(workspace_slug, agent_slug))
.map(|entry| entry.tools.clone())
.unwrap_or_default())
}
async fn refresh_if_stale(
&self,
workspace_slug: &str,
agent_slug: &str,
) -> Result<(), RegistryError> {
let key = CatalogKey::new(workspace_slug, agent_slug);
let should_refresh = {
let guard = self.cached.read().await;
match guard.get(&key).and_then(|entry| entry.loaded_at) {
Some(loaded_at) => loaded_at.elapsed() >= self.refresh_interval,
None => true,
}
};
if !should_refresh {
return Ok(());
}
if let Some(tools) = self.load_shared_snapshot(workspace_slug, agent_slug).await {
let mut guard = self.cached.write().await;
guard.insert(
key,
CachedCatalog {
loaded_at: Some(Instant::now()),
tools,
},
);
return Ok(());
}
let tools = match self
.registry
.get_published_agent_tools_by_slug(workspace_slug, agent_slug)
.await
{
Ok(tools) => tools,
Err(RegistryError::PublishedAgentNotFound { .. }) => Vec::new(),
Err(error) => return Err(error),
};
self.store_shared_snapshot(workspace_slug, agent_slug, &tools)
.await;
let mut guard = self.cached.write().await;
let previous_count = guard
.get(&key)
.map(|entry| entry.tools.len())
.unwrap_or_default();
guard.insert(
key,
CachedCatalog {
loaded_at: Some(Instant::now()),
tools,
},
);
info!(
workspace_slug,
agent_slug,
published_tool_count = guard
.get(&CatalogKey::new(workspace_slug, agent_slug))
.map(|entry| entry.tools.len())
.unwrap_or_default(),
previous_published_tool_count = previous_count,
"published agent catalog refreshed"
);
Ok(())
}
async fn load_shared_snapshot(
&self,
workspace_slug: &str,
agent_slug: &str,
) -> Option<Vec<PublishedAgentTool>> {
if self.refresh_interval.is_zero() {
return None;
}
let key = catalog_snapshot_key(workspace_slug, agent_slug);
let value = match self
.coordination_store
.get_value(CacheScope::Coordination, &key)
.await
{
Ok(value) => value?,
Err(_) => return None,
};
serde_json::from_value::<CatalogSnapshot>(value.payload)
.ok()
.map(|snapshot| snapshot.tools)
}
async fn store_shared_snapshot(
&self,
workspace_slug: &str,
agent_slug: &str,
tools: &[PublishedAgentTool],
) {
let Some(ttl) = catalog_snapshot_ttl(self.refresh_interval) else {
return;
};
let payload = match serde_json::to_value(CatalogSnapshot {
tools: tools.to_vec(),
}) {
Ok(payload) => payload,
Err(_) => return,
};
let key = catalog_snapshot_key(workspace_slug, agent_slug);
let _ = self
.coordination_store
.put_value(
CacheScope::Coordination,
&key,
CoordinationStateValue { payload },
ttl,
)
.await;
}
}
impl CatalogKey {
fn new(workspace_slug: &str, agent_slug: &str) -> Self {
Self {
workspace_slug: workspace_slug.to_owned(),
agent_slug: agent_slug.to_owned(),
}
}
}
fn catalog_snapshot_key(workspace_slug: &str, agent_slug: &str) -> String {
format!("published_catalog:{workspace_slug}:{agent_slug}")
}
fn catalog_snapshot_ttl(refresh_interval: Duration) -> Option<Duration> {
if refresh_interval.is_zero() {
return None;
}
refresh_interval.checked_mul(2).or(Some(refresh_interval))
}