use crate::base::cli::CommonCli;
use crate::base::errors::{DiscoveryError, McpError};
use crate::base::health::{HealthStatus, SubsystemDescriptor};
use crate::base::tool::ToolDescriptor;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RingId(String);
#[derive(thiserror::Error, Debug)]
pub enum RingIdError {
#[error("ring id cannot be empty")]
Empty,
#[error("ring id cannot contain whitespace")]
ContainsWhitespace,
#[error("ring id must be lowercase (got: {0})")]
NotLowercase(String),
#[error("ring id must use kebab-case, not underscores (got: {0})")]
UnderscoreNotKebab(String),
}
impl RingId {
pub fn try_new(s: impl Into<String>) -> Result<Self, RingIdError> {
let s = s.into();
if s.is_empty() {
return Err(RingIdError::Empty);
}
if s.chars().any(|c| c.is_whitespace()) {
return Err(RingIdError::ContainsWhitespace);
}
if s != s.to_lowercase() {
return Err(RingIdError::NotLowercase(s));
}
if s.contains('_') {
return Err(RingIdError::UnderscoreNotKebab(s));
}
Ok(Self(s))
}
pub fn new(s: impl Into<String>) -> Self {
Self::try_new(s).expect("invalid RingId — see RingIdError variants")
}
}
impl std::fmt::Display for RingId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl AsRef<str> for RingId {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct ConnectArgs {
pub port_override: Option<String>,
}
impl ConnectArgs {
pub fn from_cli(cli: &CommonCli) -> Result<Self, McpError> {
let port_override = match cli.port.as_deref() {
Some("") => return Err(McpError::Discovery(DiscoveryError::InvalidPort)),
other => other.map(String::from),
};
Ok(ConnectArgs { port_override })
}
}
#[async_trait]
pub trait Subsystem: Send + Sync + 'static {
fn name(&self) -> &str;
fn tools(&self) -> Vec<ToolDescriptor>;
async fn connect(&mut self, args: &ConnectArgs) -> Result<(), McpError>;
async fn disconnect(&mut self) -> Result<(), McpError>;
async fn health_check(&self) -> Result<HealthStatus, McpError>;
async fn shutdown(&mut self) -> Result<(), McpError>;
async fn invoke(&self, tool_name: &str, args: Value) -> Result<Value, McpError>;
fn version(&self) -> &str {
"unknown"
}
fn carabiner_ring(&self) -> Option<RingId> {
None
}
fn describe_self(&self) -> SubsystemDescriptor {
SubsystemDescriptor {
name: self.name().to_string(),
tool_count: self.tools().len(),
ring: self.carabiner_ring(),
transport: crate::base::cli::McpTransport::Stdio,
version: self.version().to_string(),
}
}
}