use std::collections::BTreeMap;
use std::fmt;
use std::io::{self, Write};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use asupersync::Cx;
use fastmcp_core::McpRequestCancellation;
use fastmcp_protocol::protocol_policy::ProtocolEra;
use fastmcp_protocol::{
CoreRequest, CoreResult, FinalCoreResult, ServerNotification, SubscriptionFilter,
};
use super::{ManagedOAuthSession, ManagedToolClient, ManagedToolError, ToolContract};
use crate::http_auth::rpc::catalog::watch::{
ManagedCatalogWatchControl, ManagedCatalogWatchError, ManagedCatalogWatchEvent,
ManagedCatalogWatchLimits, ManagedCatalogWatchOutcome,
};
use crate::http_auth::rpc::catalog::{
CollectedCatalog, ManagedCatalogClient, ManagedCatalogError, ManagedCatalogLimits,
};
use fastmcp_protocol::RequestId;
#[derive(Clone, Copy, Debug)]
pub struct ManagedToolCatalogLimits {
catalog: ManagedCatalogLimits,
watch: ManagedCatalogWatchLimits,
maximum_tools: usize,
maximum_definition_bytes: usize,
}
impl Default for ManagedToolCatalogLimits {
fn default() -> Self {
Self {
catalog: ManagedCatalogLimits::default(),
watch: ManagedCatalogWatchLimits::default(),
maximum_tools: 256,
maximum_definition_bytes: 4 * 1024 * 1024,
}
}
}
impl ManagedToolCatalogLimits {
pub fn new(
catalog: ManagedCatalogLimits,
watch: ManagedCatalogWatchLimits,
maximum_tools: usize,
maximum_definition_bytes: usize,
) -> Result<Self, ManagedToolCatalogError> {
if !(1..=1024).contains(&maximum_tools)
|| !(1..=8 * 1024 * 1024).contains(&maximum_definition_bytes)
{
return Err(ManagedToolCatalogError::InvalidLimits);
}
Ok(Self {
catalog,
watch,
maximum_tools,
maximum_definition_bytes,
})
}
}
#[derive(Debug)]
pub enum ManagedToolCatalogError {
InvalidLimits,
NotToolsList,
InvalidSnapshot,
ToolLimit,
DefinitionBudget,
DuplicateTool,
Invalidated,
AbortedByHost,
Tool(ManagedToolError),
Watch(ManagedCatalogWatchError),
}
impl fmt::Display for ManagedToolCatalogError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::InvalidLimits => "invalid managed tool catalog limits",
Self::NotToolsList => "tool catalog watch requires modern tools/list",
Self::InvalidSnapshot => "tool catalog snapshot contains an invalid page",
Self::ToolLimit => "managed tool catalog tool limit exceeded",
Self::DefinitionBudget => "managed tool catalog definition-byte limit exceeded",
Self::DuplicateTool => "managed tool catalog contains a duplicate tool name",
Self::Invalidated => "managed tool catalog snapshot has been invalidated",
Self::AbortedByHost => "managed tool catalog stopped by the host",
Self::Tool(error) => return fmt::Display::fmt(error, f),
Self::Watch(error) => return fmt::Display::fmt(error, f),
})
}
}
impl std::error::Error for ManagedToolCatalogError {}
pub enum ManagedToolCatalogEvent {
Acknowledged { accepted_filter: SubscriptionFilter },
Notification(Box<ServerNotification>),
Snapshot(ManagedToolCatalogSnapshot),
}
type Contracts = BTreeMap<String, Arc<ToolContract>>;
struct Snapshot {
session: ManagedOAuthSession,
catalog: CollectedCatalog,
contracts: Contracts,
invalidated: Arc<AtomicBool>,
}
#[derive(Clone)]
pub struct ManagedToolCatalogSnapshot(Arc<Snapshot>);
impl fmt::Debug for ManagedToolCatalogSnapshot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ManagedToolCatalogSnapshot")
.field("tool_count", &self.len())
.field("invalidated", &self.is_invalidated())
.finish_non_exhaustive()
}
}
impl ManagedToolCatalogSnapshot {
pub fn len(&self) -> usize {
self.0.contracts.len()
}
pub fn is_empty(&self) -> bool {
self.0.contracts.is_empty()
}
pub fn is_invalidated(&self) -> bool {
self.0.invalidated.load(Ordering::Acquire)
}
pub fn catalog(&self) -> &CollectedCatalog {
&self.0.catalog
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.0.contracts.keys().map(String::as_str)
}
pub fn tool(&self, name: &str) -> Result<Option<ManagedToolClient>, ManagedToolCatalogError> {
if self.is_invalidated() {
return Err(ManagedToolCatalogError::Invalidated);
}
let client = self
.0
.contracts
.get(name)
.map(|contract| ManagedToolClient {
session: self.0.session.clone(),
contract: Arc::clone(contract),
header_review: None,
});
if self.is_invalidated() {
return Err(ManagedToolCatalogError::Invalidated);
}
Ok(client)
}
}
impl ManagedOAuthSession {
pub async fn watch_tool_catalog<I, O>(
&self,
cx: &Cx,
request: CoreRequest,
limits: ManagedToolCatalogLimits,
next_id: I,
observe: O,
) -> Result<ManagedCatalogWatchOutcome, ManagedToolCatalogError>
where
I: FnMut() -> Result<RequestId, ManagedCatalogError>,
O: FnMut(
ManagedToolCatalogEvent,
) -> Result<ManagedCatalogWatchControl, ManagedToolCatalogError>,
{
self.watch_tool_catalog_with_cancellation(
cx,
&McpRequestCancellation::new(),
request,
limits,
next_id,
observe,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn watch_tool_catalog_with_cancellation<I, O>(
&self,
cx: &Cx,
cancellation: &McpRequestCancellation,
request: CoreRequest,
limits: ManagedToolCatalogLimits,
next_id: I,
mut observe: O,
) -> Result<ManagedCatalogWatchOutcome, ManagedToolCatalogError>
where
I: FnMut() -> Result<RequestId, ManagedCatalogError>,
O: FnMut(
ManagedToolCatalogEvent,
) -> Result<ManagedCatalogWatchControl, ManagedToolCatalogError>,
{
if request.era() != ProtocolEra::Modern2026 || request.method() != "tools/list" {
return Err(ManagedToolCatalogError::NotToolsList);
}
let collector = ManagedCatalogClient::new(self.clone(), limits.catalog);
let mut active = ActiveCatalog::default();
let mut callback_error = None;
let result = collector
.watch_with_cancellation(cx, cancellation, request, limits.watch, next_id, |event| {
let delivered = (|| {
let event = match event {
ManagedCatalogWatchEvent::Acknowledged { accepted_filter } => {
ManagedToolCatalogEvent::Acknowledged { accepted_filter }
}
ManagedCatalogWatchEvent::Notification(notification) => {
active.observe_notification(¬ification);
ManagedToolCatalogEvent::Notification(notification)
}
ManagedCatalogWatchEvent::Snapshot(catalog) => {
active.invalidate();
let (contracts, invalidated) =
admit_contracts(catalog.pages(), limits)?;
active.install_contracts(Arc::clone(&invalidated), &contracts);
ManagedToolCatalogEvent::Snapshot(ManagedToolCatalogSnapshot(Arc::new(
Snapshot {
session: self.clone(),
catalog,
contracts,
invalidated,
},
)))
}
};
observe(event)
})();
match delivered {
Ok(control) => Ok(control),
Err(error) => {
callback_error = Some(error);
Err(ManagedCatalogError::AbortedByHost)
}
}
})
.await;
drop(active);
match callback_error {
Some(error) => Err(error),
None => result.map_err(ManagedToolCatalogError::Watch),
}
}
}
#[derive(Default)]
struct ActiveCatalog(Option<Arc<AtomicBool>>, Vec<McpRequestCancellation>);
impl ActiveCatalog {
fn invalidate(&mut self) {
if let Some(invalidated) = self.0.take() {
invalidated.store(true, Ordering::Release);
}
for signal in self.1.drain(..) {
signal.cancel();
}
}
fn install(&mut self, invalidated: Arc<AtomicBool>) {
self.invalidate();
self.0 = Some(invalidated);
}
fn install_contracts(&mut self, invalidated: Arc<AtomicBool>, contracts: &Contracts) {
self.install(invalidated);
self.1.extend(
contracts
.values()
.map(|contract| contract.invalidation.clone()),
);
}
fn observe_notification(&mut self, notification: &ServerNotification) {
if matches!(notification, ServerNotification::ToolsListChanged(_)) {
self.invalidate();
}
}
}
impl Drop for ActiveCatalog {
fn drop(&mut self) {
self.invalidate();
}
}
fn admit_contracts(
pages: &[CoreResult],
limits: ManagedToolCatalogLimits,
) -> Result<(Contracts, Arc<AtomicBool>), ManagedToolCatalogError> {
if pages.is_empty() {
return Err(ManagedToolCatalogError::InvalidSnapshot);
}
let invalidated = Arc::new(AtomicBool::new(false));
let mut contracts = BTreeMap::new();
let mut bytes = DefinitionBytes {
used: 0,
maximum: limits.maximum_definition_bytes,
};
for page in pages {
let CoreResult::Final(FinalCoreResult::ToolsList { result, .. }) = page else {
return Err(ManagedToolCatalogError::InvalidSnapshot);
};
for definition in &result.payload.tools {
if contracts.len() >= limits.maximum_tools {
return Err(ManagedToolCatalogError::ToolLimit);
}
serde_json::to_writer(&mut bytes, definition)
.map_err(|_| ManagedToolCatalogError::DefinitionBudget)?;
if contracts.contains_key(&definition.name) {
return Err(ManagedToolCatalogError::DuplicateTool);
}
let mut contract =
ToolContract::admit(definition.clone()).map_err(ManagedToolCatalogError::Tool)?;
contract.catalog_invalidated = Some(Arc::clone(&invalidated));
contracts.insert(contract.name.clone(), Arc::new(contract));
}
}
Ok((contracts, invalidated))
}
struct DefinitionBytes {
used: usize,
maximum: usize,
}
impl Write for DefinitionBytes {
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
if buffer.len() > self.maximum.saturating_sub(self.used) {
return Err(io::Error::other("managed tool definition byte limit"));
}
self.used += buffer.len();
Ok(buffer.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod wake_tests;