use std::sync::{Arc, RwLock};
use rmcp::{
model::{GetPromptRequestParams, ReadResourceRequestParams},
Peer, RoleClient,
};
use super::{result, session::McpServerOffers};
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct McpPrompt {
pub(crate) server: String,
pub(crate) name: String,
pub(crate) title: Option<String>,
pub(crate) description: Option<String>,
pub(crate) arguments: Vec<McpPromptArgument>,
}
impl McpPrompt {
pub(crate) fn command_name(&self) -> String {
format!("mcp:{}:{}", self.server, self.name)
}
pub(crate) fn label(&self) -> &str {
self.title.as_deref().unwrap_or(&self.name)
}
pub(crate) fn usage(&self) -> String {
let mut usage = format!("/{}", self.command_name());
for argument in &self.arguments {
if argument.required {
usage.push_str(&format!(" <{}>", argument.name));
} else {
usage.push_str(&format!(" [{}=…]", argument.name));
}
}
usage
}
pub(crate) fn parse_arguments(
&self,
trailing: &str,
) -> serde_json::Map<String, serde_json::Value> {
let trailing = trailing.trim();
let mut arguments = serde_json::Map::new();
if trailing.is_empty() {
return arguments;
}
if let [only] = self.arguments.as_slice() {
arguments.insert(only.name.clone(), trailing.into());
return arguments;
}
for pair in trailing.split_whitespace() {
if let Some((name, value)) = pair.split_once('=') {
arguments.insert(name.to_string(), value.into());
}
}
arguments
}
pub(crate) fn missing_arguments(
&self,
supplied: &serde_json::Map<String, serde_json::Value>,
) -> Vec<&str> {
self.arguments
.iter()
.filter(|argument| argument.required && !supplied.contains_key(&argument.name))
.map(|argument| argument.name.as_str())
.collect()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct McpPromptArgument {
pub(crate) name: String,
pub(crate) description: Option<String>,
pub(crate) required: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct McpResource {
pub(crate) server: String,
pub(crate) uri: String,
pub(crate) name: String,
pub(crate) title: Option<String>,
pub(crate) description: Option<String>,
pub(crate) mime_type: Option<String>,
pub(crate) templated: bool,
}
impl McpResource {
pub(crate) fn label(&self) -> &str {
self.title.as_deref().unwrap_or(&self.name)
}
}
#[derive(Debug, Default)]
struct McpCatalogEntry {
prompts: Vec<McpPrompt>,
resources: Vec<McpResource>,
}
#[derive(Debug)]
struct McpCatalogServer {
identity: String,
peer: Peer<RoleClient>,
offers: McpServerOffers,
entry: RwLock<McpCatalogEntry>,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct McpCatalog {
servers: Arc<RwLock<Vec<Arc<McpCatalogServer>>>>,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum McpCatalogError {
UnknownServer(String),
Server(String),
}
impl std::fmt::Display for McpCatalogError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownServer(identity) => {
write!(formatter, "no connected MCP server named `{identity}`")
}
Self::Server(message) => formatter.write_str(message),
}
}
}
impl std::error::Error for McpCatalogError {}
#[derive(Debug, PartialEq)]
pub(crate) struct McpPromptExpansion {
pub(crate) description: Option<String>,
pub(crate) text: String,
}
impl McpCatalog {
pub(super) fn register(
&self,
identity: String,
peer: Peer<RoleClient>,
offers: McpServerOffers,
) -> McpCatalogHandle {
let server = Arc::new(McpCatalogServer {
identity,
peer,
offers,
entry: RwLock::new(McpCatalogEntry::default()),
});
self.write().push(Arc::clone(&server));
McpCatalogHandle { server }
}
pub(crate) fn is_empty(&self) -> bool {
self.read()
.iter()
.all(|server| server.entry().prompts.is_empty() && server.entry().resources.is_empty())
}
pub(crate) fn prompts(&self) -> Vec<McpPrompt> {
self.collect(|entry| entry.prompts.clone())
}
pub(crate) fn resources(&self) -> Vec<McpResource> {
self.collect(|entry| entry.resources.clone())
}
fn collect<T>(&self, select: impl Fn(&McpCatalogEntry) -> Vec<T>) -> Vec<T> {
let mut servers = self.read().clone();
servers.sort_by(|left, right| left.identity.cmp(&right.identity));
servers
.iter()
.flat_map(|server| select(&server.entry()))
.collect()
}
pub(crate) async fn get_prompt(
&self,
server: &str,
name: &str,
arguments: serde_json::Map<String, serde_json::Value>,
max_output_bytes: usize,
) -> Result<McpPromptExpansion, McpCatalogError> {
let peer = self.peer(server)?;
let mut params = GetPromptRequestParams::new(name);
if !arguments.is_empty() {
params.arguments = Some(arguments);
}
let result = peer
.get_prompt(params)
.await
.map_err(|error| McpCatalogError::Server(error.to_string()))?;
Ok(McpPromptExpansion {
description: result.description.clone(),
text: result::render_prompt_messages(&result.messages, max_output_bytes),
})
}
pub(crate) async fn read_resource(
&self,
server: &str,
uri: &str,
) -> Result<Vec<McpResourceContent>, McpCatalogError> {
let peer = self.peer(server)?;
let result = peer
.read_resource(ReadResourceRequestParams::new(uri))
.await
.map_err(|error| McpCatalogError::Server(error.to_string()))?;
Ok(result
.contents
.iter()
.map(McpResourceContent::from_remote)
.collect())
}
fn peer(&self, identity: &str) -> Result<Peer<RoleClient>, McpCatalogError> {
self.read()
.iter()
.find(|server| server.identity == identity)
.map(|server| server.peer.clone())
.ok_or_else(|| McpCatalogError::UnknownServer(identity.to_string()))
}
fn read(&self) -> std::sync::RwLockReadGuard<'_, Vec<Arc<McpCatalogServer>>> {
self.servers
.read()
.unwrap_or_else(|error| error.into_inner())
}
fn write(&self) -> std::sync::RwLockWriteGuard<'_, Vec<Arc<McpCatalogServer>>> {
self.servers
.write()
.unwrap_or_else(|error| error.into_inner())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum McpCompletionSupport {
Declared,
Absent,
}
impl McpCatalog {
pub(crate) fn completion_support(&self, server: &str) -> McpCompletionSupport {
if self.offers(server).is_some_and(|offers| offers.completions) {
McpCompletionSupport::Declared
} else {
McpCompletionSupport::Absent
}
}
pub(crate) async fn complete_prompt_argument(
&self,
server: &str,
prompt: &str,
argument: &str,
typed: &str,
) -> Vec<String> {
if self.completion_support(server) == McpCompletionSupport::Absent {
return Vec::new();
}
let Ok(peer) = self.peer(server) else {
return Vec::new();
};
peer.complete_prompt_simple(prompt, argument, typed)
.await
.unwrap_or_default()
}
fn offers(&self, identity: &str) -> Option<McpServerOffers> {
self.read()
.iter()
.find(|server| server.identity == identity)
.map(|server| server.offers)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum McpResourceContent {
Text {
uri: String,
mime_type: Option<String>,
text: String,
},
Blob {
uri: String,
mime_type: Option<String>,
blob: String,
},
Unsupported,
}
impl McpResourceContent {
fn from_remote(remote: &rmcp::model::ResourceContents) -> Self {
match remote {
rmcp::model::ResourceContents::TextResourceContents {
uri,
mime_type,
text,
..
} => Self::Text {
uri: uri.clone(),
mime_type: mime_type.clone(),
text: text.clone(),
},
rmcp::model::ResourceContents::BlobResourceContents {
uri,
mime_type,
blob,
..
} => Self::Blob {
uri: uri.clone(),
mime_type: mime_type.clone(),
blob: blob.clone(),
},
_ => Self::Unsupported,
}
}
}
impl McpCatalogServer {
fn entry(&self) -> std::sync::RwLockReadGuard<'_, McpCatalogEntry> {
self.entry.read().unwrap_or_else(|error| error.into_inner())
}
}
#[derive(Debug)]
pub(super) struct McpCatalogHandle {
server: Arc<McpCatalogServer>,
}
impl McpCatalogHandle {
pub(super) fn identity(&self) -> &str {
&self.server.identity
}
pub(super) fn peer(&self) -> &Peer<RoleClient> {
&self.server.peer
}
pub(super) fn set_prompts(&self, prompts: Vec<McpPrompt>) {
self.write().prompts = prompts;
}
pub(super) fn set_resources(&self, resources: Vec<McpResource>) {
self.write().resources = resources;
}
fn write(&self) -> std::sync::RwLockWriteGuard<'_, McpCatalogEntry> {
self.server
.entry
.write()
.unwrap_or_else(|error| error.into_inner())
}
}
pub(super) fn prompts_from_remote(
identity: &str,
remote: Vec<rmcp::model::Prompt>,
) -> Vec<McpPrompt> {
let mut prompts = remote
.into_iter()
.map(|prompt| McpPrompt {
server: identity.to_string(),
name: prompt.name,
title: prompt.title,
description: prompt.description,
arguments: prompt
.arguments
.unwrap_or_default()
.into_iter()
.map(|argument| McpPromptArgument {
name: argument.name,
description: argument.description,
required: argument.required.unwrap_or(false),
})
.collect(),
})
.collect::<Vec<_>>();
prompts.sort_by(|left, right| left.name.cmp(&right.name));
prompts
}
pub(super) fn resources_from_remote(
identity: &str,
concrete: Vec<rmcp::model::Resource>,
templates: Vec<rmcp::model::ResourceTemplate>,
) -> Vec<McpResource> {
let mut resources = concrete
.into_iter()
.map(|resource| McpResource {
server: identity.to_string(),
uri: resource.uri,
name: resource.name,
title: resource.title,
description: resource.description,
mime_type: resource.mime_type,
templated: false,
})
.chain(templates.into_iter().map(|template| McpResource {
server: identity.to_string(),
uri: template.uri_template,
name: template.name,
title: template.title,
description: template.description,
mime_type: template.mime_type,
templated: true,
}))
.collect::<Vec<_>>();
resources.sort_by(|left, right| left.name.cmp(&right.name).then(left.uri.cmp(&right.uri)));
resources
}
#[cfg(test)]
#[path = "catalog_tests.rs"]
mod tests;