feat(mcp): gate meta-function emission on advertised server capabilities

Per-server McpServerFeatures (tools fail-open, resources/prompts
fail-closed) now drive which meta-functions are declared, with
gated_meta_function_prefixes as the single gating seam; read/prompt
declarations land together with their handlers. The server-enablement
sentinel keys on the always-emitted search name so resources-only
servers survive role filtering.

Implements plans/mcp-resources-prompts-design.md §4.4/D7 (T5).
This commit is contained in:
2026-08-25 11:37:41 -06:00
parent ef88b6a2c8
commit 437512fd6d
8 changed files with 433 additions and 87 deletions
+38 -3
View File
@@ -16,6 +16,7 @@ use futures_util::{StreamExt, TryStreamExt, stream};
use http::{HeaderName, HeaderValue};
use indexmap::IndexMap;
use indoc::formatdoc;
use rmcp::model::ServerCapabilities;
use rmcp::service::RunningService;
use rmcp::transport::StreamableHttpClientTransport;
use rmcp::transport::TokioChildProcess;
@@ -61,6 +62,28 @@ pub fn mcp_meta_function_names(server: &str) -> Vec<String> {
pub type ConnectedServer = RunningService<RoleClient, ()>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct McpServerFeatures {
pub name: String,
pub tools: bool,
pub resources: bool,
pub prompts: bool,
}
impl McpServerFeatures {
pub fn from_capabilities(
name: impl Into<String>,
capabilities: Option<&ServerCapabilities>,
) -> Self {
Self {
name: name.into(),
tools: capabilities.is_none_or(|c| c.tools.is_some()),
resources: capabilities.is_some_and(|c| c.resources.is_some()),
prompts: capabilities.is_some_and(|c| c.prompts.is_some()),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogItemKind {
@@ -417,8 +440,20 @@ impl McpRegistry {
&self.servers
}
pub fn list_started_servers(&self) -> Vec<String> {
self.servers.keys().cloned().collect()
pub fn server_features(&self) -> Vec<McpServerFeatures> {
let mut features: Vec<McpServerFeatures> = self
.servers
.iter()
.map(|(name, handle)| {
let info = handle.peer_info();
McpServerFeatures::from_capabilities(
name.as_str(),
info.as_ref().map(|info| &info.capabilities),
)
})
.collect();
features.sort_by(|a, b| a.name.cmp(&b.name));
features
}
pub fn is_empty(&self) -> bool {
@@ -1180,7 +1215,7 @@ mod tests {
let registry = McpRegistry::default();
assert!(registry.is_empty());
assert!(registry.list_started_servers().is_empty());
assert!(registry.server_features().is_empty());
assert!(registry.mcp_config().is_none());
assert!(registry.log_path().is_none());
}