jumperless-mcp 0.1.0

MCP server for the Jumperless V5 — persistent USB-serial bridge exposing the firmware API to LLMs
//! `base` — bundled MCP substrate (folded in at publish time by `scripts/publish-jumperless-mcp.py`).
//!
//! Substrate for MCP servers — common runtime, transport, error hierarchy.
//!
//! Provides the common runtime, transport, error hierarchy, and [`Subsystem`]
//! trait so each subsystem-specific MCP crate only needs to declare its tool
//! surface and protocol-specific logic.
//!
//! # Quick start
//!
//! ```rust,ignore
//! use ygg_mcp_base::{CommonCli, Subsystem, run};
//! use clap::Parser;
//!
//! struct MySubsystem;
//!
//! #[async_trait::async_trait]
//! impl Subsystem for MySubsystem {
//!     fn name(&self) -> &str { "my-subsystem" }
//!     // ... implement the other required methods
//! }
//!
//! #[tokio::main]
//! async fn main() -> Result<(), ygg_mcp_base::McpError> {
//!     let cli = CommonCli::parse();
//!     run(MySubsystem, cli).await
//! }
//! ```

pub mod cli;
pub mod discovery;
pub mod errors;
pub mod federation;
pub mod health;
pub mod subsystem;
pub mod tool;
pub mod transport;
pub mod usb;
pub mod wire;

// ── Re-exports: the public API surface ───────────────────────────────────────

pub use cli::{CommonCli, LogLevel, McpTransport};
pub use discovery::{KeywordSearchEngine, ListMode, Scope, SearchEngine, ToolHit};
pub use errors::{DiscoveryError, McpError};
pub use federation::FederationAggregator;
pub use health::{HealthLevel, HealthStatus, SubsystemDescriptor};
pub use subsystem::{ConnectArgs, RingId, RingIdError, Subsystem};
pub use tool::{ToolDescriptor, ToolDescriptorError};
pub use transport::serve_mcp;
pub use usb::{
    discover_by_name_pattern, discover_by_vid_pid, discover_composite_port_by_index, VidPid,
};
pub use wire::{WireFormat, WireNameTranslator};

// ── Top-level entrypoints ─────────────────────────────────────────────────────

/// Initialize tracing. Called by [`run`] and [`run_federation`] before the
/// MCP server loop starts.
///
/// Reads the per-subsystem env var via `EnvFilter::from_default_env()`; falls
/// back to `log_level` if the env var is absent.
pub fn init_tracing(log_level: cli::LogLevel) {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(log_level.as_filter())),
        )
        .init();
}

/// Single-subsystem deployment entrypoint.
///
/// 1. Initialises tracing.
/// 2. Calls [`Subsystem::connect`] with args derived from `cli`.
/// 3. Runs the MCP server loop ([`serve_mcp`]) over stdio until the client
///    disconnects or an unrecoverable error occurs.
///
/// Most subsystem MCPs call this from `main`.
pub async fn run<S: Subsystem>(subsystem: S, cli: CommonCli) -> Result<(), McpError> {
    init_tracing(cli.log_level);
    let mut s = subsystem;
    let connect_args = ConnectArgs::from_cli(&cli)?;
    s.connect(&connect_args).await?;
    let transport = cli.transport.clone();
    serve_mcp(s, transport).await
}

/// MCP-of-MCPs deployment entrypoint.
///
/// Wraps multiple `Subsystem`s in a [`FederationAggregator`], connects all
/// members, and serves one composite MCP endpoint over stdio until the client
/// disconnects or an unrecoverable error occurs.
///
/// Uses [`WireFormat::default()`] (underscore-separated) for safe OSS distribution.
/// Pass a custom `WireFormat` directly to `FederationAggregator::new()` if needed.
pub async fn run_federation(
    subsystems: Vec<Box<dyn Subsystem>>,
    cli: CommonCli,
) -> Result<(), McpError> {
    init_tracing(cli.log_level);
    let mut aggregator = FederationAggregator::new(subsystems, WireFormat::default());
    let connect_args = ConnectArgs::from_cli(&cli)?;
    aggregator.connect_all(&connect_args).await?;
    let transport = cli.transport.clone();
    serve_mcp(aggregator, transport).await
}