use crate::cli::McpOptions;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[allow(dead_code)] pub(crate) enum Capability {
ReadOnlyDb,
DestructiveWrite,
Shell,
}
impl Capability {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::ReadOnlyDb => "ReadOnlyDb",
Self::DestructiveWrite => "DestructiveWrite",
Self::Shell => "Shell",
}
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct CapabilitySet {
read_only_db: bool,
destructive_write: bool,
}
impl CapabilitySet {
pub(crate) fn from_options(options: &McpOptions) -> Self {
Self {
read_only_db: true,
destructive_write: options.allow_destructive_writes,
}
}
#[cfg(test)]
pub(crate) fn for_test(read_only_db: bool, destructive_write: bool) -> Self {
Self {
read_only_db,
destructive_write,
}
}
pub(crate) fn grants(&self, capability: Capability) -> bool {
match capability {
Capability::ReadOnlyDb => self.read_only_db,
Capability::DestructiveWrite => self.destructive_write,
Capability::Shell => false,
}
}
pub(crate) fn check(
&self,
required: &[Capability],
) -> Result<(), crate::commands::mcp::error::McpError> {
use crate::commands::mcp::error::McpError;
for capability in required {
if !self.grants(*capability) {
return Err(McpError::CapabilityRefused {
capability: capability.as_str(),
});
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::McpOptions;
fn default_options() -> McpOptions {
McpOptions::default()
}
#[test]
fn default_grants_read_only_db_not_destructive() {
let caps = CapabilitySet::from_options(&default_options());
assert!(caps.grants(Capability::ReadOnlyDb));
assert!(!caps.grants(Capability::DestructiveWrite));
}
#[test]
fn shell_is_never_granted_regardless_of_flags() {
let caps = CapabilitySet::from_options(&McpOptions {
allow_destructive_writes: true,
});
assert!(!caps.grants(Capability::Shell));
}
#[test]
fn destructive_write_requires_the_flag() {
let on = CapabilitySet::from_options(&McpOptions {
allow_destructive_writes: true,
});
assert!(on.grants(Capability::DestructiveWrite));
let off = CapabilitySet::from_options(&default_options());
assert!(!off.grants(Capability::DestructiveWrite));
}
#[test]
fn empty_requirements_pass_with_any_capability_set() {
let caps = CapabilitySet::from_options(&default_options());
assert!(caps.check(&[]).is_ok());
}
#[test]
fn read_only_db_passes_by_default() {
let caps = CapabilitySet::from_options(&default_options());
assert!(caps.check(&[Capability::ReadOnlyDb]).is_ok());
}
#[test]
fn destructive_write_refused_by_default() {
let caps = CapabilitySet::from_options(&default_options());
let err = caps
.check(&[Capability::DestructiveWrite])
.expect_err("refused");
assert!(
matches!(err, crate::commands::mcp::error::McpError::CapabilityRefused { capability } if capability == "DestructiveWrite")
);
}
#[test]
fn destructive_write_passes_when_flagged() {
let caps = CapabilitySet::from_options(&McpOptions {
allow_destructive_writes: true,
});
assert!(caps.check(&[Capability::DestructiveWrite]).is_ok());
}
#[test]
fn shell_is_always_refused_even_if_a_tool_requires_it() {
let caps = CapabilitySet::for_test(true, true);
let err = caps.check(&[Capability::Shell]).expect_err("refused");
assert!(
matches!(err, crate::commands::mcp::error::McpError::CapabilityRefused { capability } if capability == "Shell")
);
}
#[test]
fn check_reports_the_first_ungranted_capability() {
let caps = CapabilitySet::from_options(&default_options());
let err = caps
.check(&[Capability::DestructiveWrite, Capability::Shell])
.expect_err("refused");
assert!(
matches!(err, crate::commands::mcp::error::McpError::CapabilityRefused { capability } if capability == "DestructiveWrite")
);
}
}